From 994bac4896650ecaeee7c81e2084872509a4b815 Mon Sep 17 00:00:00 2001 From: Andrei Bombin Date: Fri, 20 Jun 2025 10:50:25 -0400 Subject: [PATCH 001/102] feat(qc-metrics): add common single cell quality control metrics --- src/spac/transformations.py | 91 ++++++++++++++++++- .../test_add_qc_metrics.py | 62 +++++++++++++ 2 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 tests/test_transformations/test_add_qc_metrics.py diff --git a/src/spac/transformations.py b/src/spac/transformations.py index b2044f1c..e32be301 100644 --- a/src/spac/transformations.py +++ b/src/spac/transformations.py @@ -8,7 +8,7 @@ from spac.utils import check_table, check_annotation, check_feature from scipy import stats import umap as umap_lib -from scipy.sparse import issparse +from scipy.sparse import issparse, csr_matrix from typing import List, Union, Optional from numpy.lib import NumpyVersion from sklearn.neighbors import KNeighborsClassifier @@ -1286,3 +1286,92 @@ def run_utag_clustering( cluster_list = utag_results.obs[cur_cluster_col].copy() adata.obs[output_annotation] = cluster_list.copy() adata.uns["utag_features"] = features + +# add QC metrics to AnnData object +def add_qc_metrics(adata, + organism="hs", + mt_match_pattern=None, + layer=None): + """ + Adds quality control (QC) metrics to the AnnData object. + + Parameters: + ----------- + adata : AnnData + The AnnData object containing single-cell or spatial + transcriptomics data. + organism : str, optional + The organism type. Default is "hs" (human). Use "mm" for mouse. + Determines the mitochondrial gene prefix + ("MT-" for human, "mt-" for mouse). + mt_match_pattern : str, optional + A custom pattern to identify mitochondrial genes. If None, it defaults + to "MT-" for human or "mt-" for mouse based on the `organism` parameter. + Takes precedence over the default patterns. + If provided, it should match the prefix of mitochondrial gene names in + `adata.var_names`. + layer : str, optional + The name of the layer in `adata.layers` to use for calculations. + If None, the default `adata.X` matrix is used. + + Modifies: + --------- + adata.obs : pandas.DataFrame + Adds the following QC metrics as new columns: + - "nFeatue": Number of genes with non-zero expression for each cell. + - "nCount": Total counts (sum of all gene expression values) + for each cell. + - "nCount_mt": Total counts for mitochondrial genes for each cell. + - "percent.mt": Percentage of counts in mitochondrial genes + for each cell. + + Raises: + ------- + ValueError + If the specified `layer` is not found in `adata.layers`. + + Notes: + ------ + - If the input matrix (`adata.X` or the specified layer) is dense, + it is converted to a sparse matrix for efficient computation. + - Mitochondrial genes are identified based on the `mt_match_pattern`. + + Example: + -------- + >>> add_qc_metrics(adata, organism="hs") + >>> print(adata.obs[["nFeatue", "nCount", "nCount_mt", "percent.mt"]]) + """ + # identify mitochondrial genes pattern + if mt_match_pattern is None: + if organism == "hs": + mt_match_pattern = "MT-" + elif organism == "mm": + mt_match_pattern = "mt-" + + if layer is None: + test_matrix = adata.X + else: + if layer not in adata.layers: + raise ValueError(f"Layer '{layer}' not found in adata.layers.") + test_matrix = adata.layers[layer] + + # Check if adata.X is sparse, and convert if necessary + if not issparse(test_matrix): + test_matrix = csr_matrix(test_matrix) + + # Calculate total number of genes with values > 0 for each cell + adata.obs["nFeatue"] = np.array((test_matrix > 0).sum(axis=1)).flatten() + # Calculate the sum of counts for all genes for each cell + adata.obs["nCount"] = np.array(test_matrix.sum(axis=1)).flatten() + # Identify mitochondrial genes based on the match pattern + mt_genes = adata.var_names.str.startswith(mt_match_pattern) + # Calculate the sum of counts for mitochondrial genes for each cell + adata.obs["nCount_mt"] = np.array(test_matrix[:, mt_genes] + .sum(axis=1)).flatten() + # Calculate the percentage of counts in mitochondrial genes for each cell + adata.obs["percent.mt"] = (adata.obs["nCount_mt"] / + adata.obs["nCount"]) * 100 + # Handle NaN values in percent.mt + adata.obs["percent.mt"] = adata.obs["percent.mt"].fillna(0) + # Ensure percent.mt is stored as a float + adata.obs["percent.mt"] = adata.obs["percent.mt"].astype(float) \ No newline at end of file diff --git a/tests/test_transformations/test_add_qc_metrics.py b/tests/test_transformations/test_add_qc_metrics.py new file mode 100644 index 00000000..5b77e6ec --- /dev/null +++ b/tests/test_transformations/test_add_qc_metrics.py @@ -0,0 +1,62 @@ +import unittest +import numpy as np +import scanpy as sc +from scipy.sparse import csr_matrix +from spac.transformations import add_qc_metrics + +class TestAddQCMetrics(unittest.TestCase): + @classmethod + def setUpClass(cls): + np.random.seed(42) + + def create_test_adata(self, sparse=False): + X = np.array([ + [1, 0, 3, 0], + [0, 2, 0, 4], + [5, 0, 0, 6] + ]) + var_names = ["MT-CO1", "MT-CO2", "GeneA", "GeneB"] + obs_names = ["cell1", "cell2", "cell3"] + adata = sc.AnnData(X=csr_matrix(X) if sparse else X) + adata.var_names = var_names + adata.obs_names = obs_names + return adata + + def test_qc_metrics_dense(self): + adata = self.create_test_adata(sparse=False) + add_qc_metrics(adata, organism="hs") + self.assertIn("nFeatue", adata.obs) + self.assertIn("nCount", adata.obs) + self.assertIn("nCount_mt", adata.obs) + self.assertIn("percent.mt", adata.obs) + np.testing.assert_array_equal(adata.obs["nFeatue"].values, [2, 2, 2]) + np.testing.assert_array_equal(adata.obs["nCount"].values, [4, 6, 11]) + np.testing.assert_array_equal(adata.obs["nCount_mt"].values, [1, 2, 5]) + np.testing.assert_allclose(adata.obs["percent.mt"].values, + [25.0, 33.333333, 45.454545], rtol=1e-4) + + def test_qc_metrics_sparse(self): + adata = self.create_test_adata(sparse=True) + add_qc_metrics(adata, organism="hs") + self.assertIn("nFeatue", adata.obs) + self.assertIn("nCount", adata.obs) + self.assertIn("nCount_mt", adata.obs) + self.assertIn("percent.mt", adata.obs) + np.testing.assert_array_equal(adata.obs["nFeatue"].values, [2, 2, 2]) + np.testing.assert_array_equal(adata.obs["nCount"].values, [4, 6, 11]) + np.testing.assert_array_equal(adata.obs["nCount_mt"].values, [1, 2, 5]) + np.testing.assert_allclose(adata.obs["percent.mt"].values, + [25.0, 33.333333, 45.454545], rtol=1e-4) + + def test_custom_mt_pattern(self): + adata = self.create_test_adata() + add_qc_metrics(adata, mt_match_pattern="Gene") + np.testing.assert_array_equal(adata.obs["nCount_mt"].values, [3, 4, 6]) + + def test_invalid_layer(self): + adata = self.create_test_adata() + with self.assertRaises(ValueError): + add_qc_metrics(adata, layer="not_a_layer") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 59675cad6540787be3a8a8a300dcc6c7e398dec9 Mon Sep 17 00:00:00 2001 From: Andrei Bombin Date: Fri, 20 Jun 2025 11:17:00 -0400 Subject: [PATCH 002/102] style(qc-metrics): fix spelling typo in nFeature metric --- src/spac/transformations.py | 6 +++--- tests/test_transformations/test_add_qc_metrics.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/spac/transformations.py b/src/spac/transformations.py index e32be301..ff1d29ed 100644 --- a/src/spac/transformations.py +++ b/src/spac/transformations.py @@ -1318,7 +1318,7 @@ def add_qc_metrics(adata, --------- adata.obs : pandas.DataFrame Adds the following QC metrics as new columns: - - "nFeatue": Number of genes with non-zero expression for each cell. + - "nFeature": Number of genes with non-zero expression for each cell. - "nCount": Total counts (sum of all gene expression values) for each cell. - "nCount_mt": Total counts for mitochondrial genes for each cell. @@ -1339,7 +1339,7 @@ def add_qc_metrics(adata, Example: -------- >>> add_qc_metrics(adata, organism="hs") - >>> print(adata.obs[["nFeatue", "nCount", "nCount_mt", "percent.mt"]]) + >>> print(adata.obs[["nFeature", "nCount", "nCount_mt", "percent.mt"]]) """ # identify mitochondrial genes pattern if mt_match_pattern is None: @@ -1360,7 +1360,7 @@ def add_qc_metrics(adata, test_matrix = csr_matrix(test_matrix) # Calculate total number of genes with values > 0 for each cell - adata.obs["nFeatue"] = np.array((test_matrix > 0).sum(axis=1)).flatten() + adata.obs["nFeature"] = np.array((test_matrix > 0).sum(axis=1)).flatten() # Calculate the sum of counts for all genes for each cell adata.obs["nCount"] = np.array(test_matrix.sum(axis=1)).flatten() # Identify mitochondrial genes based on the match pattern diff --git a/tests/test_transformations/test_add_qc_metrics.py b/tests/test_transformations/test_add_qc_metrics.py index 5b77e6ec..65d650fb 100644 --- a/tests/test_transformations/test_add_qc_metrics.py +++ b/tests/test_transformations/test_add_qc_metrics.py @@ -25,11 +25,11 @@ def create_test_adata(self, sparse=False): def test_qc_metrics_dense(self): adata = self.create_test_adata(sparse=False) add_qc_metrics(adata, organism="hs") - self.assertIn("nFeatue", adata.obs) + self.assertIn("nFeature", adata.obs) self.assertIn("nCount", adata.obs) self.assertIn("nCount_mt", adata.obs) self.assertIn("percent.mt", adata.obs) - np.testing.assert_array_equal(adata.obs["nFeatue"].values, [2, 2, 2]) + np.testing.assert_array_equal(adata.obs["nFeature"].values, [2, 2, 2]) np.testing.assert_array_equal(adata.obs["nCount"].values, [4, 6, 11]) np.testing.assert_array_equal(adata.obs["nCount_mt"].values, [1, 2, 5]) np.testing.assert_allclose(adata.obs["percent.mt"].values, @@ -38,11 +38,11 @@ def test_qc_metrics_dense(self): def test_qc_metrics_sparse(self): adata = self.create_test_adata(sparse=True) add_qc_metrics(adata, organism="hs") - self.assertIn("nFeatue", adata.obs) + self.assertIn("nFeature", adata.obs) self.assertIn("nCount", adata.obs) self.assertIn("nCount_mt", adata.obs) self.assertIn("percent.mt", adata.obs) - np.testing.assert_array_equal(adata.obs["nFeatue"].values, [2, 2, 2]) + np.testing.assert_array_equal(adata.obs["nFeature"].values, [2, 2, 2]) np.testing.assert_array_equal(adata.obs["nCount"].values, [4, 6, 11]) np.testing.assert_array_equal(adata.obs["nCount_mt"].values, [1, 2, 5]) np.testing.assert_allclose(adata.obs["percent.mt"].values, From 7bf404337bf9fdbf7048ecd0725411aff98ff46b Mon Sep 17 00:00:00 2001 From: Andrei Bombin Date: Fri, 20 Jun 2025 11:21:28 -0400 Subject: [PATCH 003/102] Update src/spac/transformations.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/spac/transformations.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/spac/transformations.py b/src/spac/transformations.py index ff1d29ed..2f2a75be 100644 --- a/src/spac/transformations.py +++ b/src/spac/transformations.py @@ -1347,6 +1347,8 @@ def add_qc_metrics(adata, mt_match_pattern = "MT-" elif organism == "mm": mt_match_pattern = "mt-" + else: + raise ValueError(f"Unsupported organism '{organism}'. Supported values are 'hs' and 'mm'.") if layer is None: test_matrix = adata.X From 0cf530bb603c0d74beeaa797df5f8ad222512921 Mon Sep 17 00:00:00 2001 From: Andrei Bombin Date: Mon, 23 Jun 2025 13:42:36 -0400 Subject: [PATCH 004/102] fix(check_layer): use check_table spac function to evaluate if adata.layer is present --- src/spac/transformations.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/spac/transformations.py b/src/spac/transformations.py index 2f2a75be..37fa04de 100644 --- a/src/spac/transformations.py +++ b/src/spac/transformations.py @@ -1353,8 +1353,7 @@ def add_qc_metrics(adata, if layer is None: test_matrix = adata.X else: - if layer not in adata.layers: - raise ValueError(f"Layer '{layer}' not found in adata.layers.") + check_table(adata, tables=layer) test_matrix = adata.layers[layer] # Check if adata.X is sparse, and convert if necessary From a228e5eb33777d03bec96af826568545e44157fd Mon Sep 17 00:00:00 2001 From: Andrei Bombin Date: Thu, 3 Jul 2025 15:21:35 -0400 Subject: [PATCH 005/102] feat(qc_summary_statistics): add summary statistics table for sc/spatial transcriptomics quality control metrics --- src/spac/transformations.py | 86 ++++++++++++++++++- .../test_get_qc_summary_table.py | 83 ++++++++++++++++++ 2 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 tests/test_transformations/test_get_qc_summary_table.py diff --git a/src/spac/transformations.py b/src/spac/transformations.py index 37fa04de..09f4f7bc 100644 --- a/src/spac/transformations.py +++ b/src/spac/transformations.py @@ -16,6 +16,8 @@ import multiprocessing import parmap from spac.utag_functions import utag +from scipy.stats import median_abs_deviation +from anndata import AnnData # Configure logging logging.basicConfig(level=logging.INFO, @@ -1375,4 +1377,86 @@ def add_qc_metrics(adata, # Handle NaN values in percent.mt adata.obs["percent.mt"] = adata.obs["percent.mt"].fillna(0) # Ensure percent.mt is stored as a float - adata.obs["percent.mt"] = adata.obs["percent.mt"].astype(float) \ No newline at end of file + adata.obs["percent.mt"] = adata.obs["percent.mt"].astype(float) + +# Add the QC summary table to AnnData object +def get_qc_summary_table( + adata: AnnData, + n_mad: int = 5, + upper_quantile: float = 0.95, + lower_quantile: float = 0.05, + stat_columns_list: list = ["nFeature", "nCount", "percent.mt"], + sample_column: str = None +) -> None: + """ + Compute summary statistics for quality control metrics in an AnnData object + and store the result in adata.uns['qc_summary_table']. + + Parameters: + adata (AnnData): The AnnData object containing the data. + n_mad (int): Number of MADs to use for upper/lower thresholds. + upper_quantile (float): Upper quantile to compute (e.g., 0.95). + lower_quantile (float): Lower quantile to compute (e.g., 0.05). + stat_columns_list (list): List of column names to compute statistics for. + sample_column (str, optional): Column name to group by sample. + If None, computes for all data. + + Returns: + None. The summary table is stored in adata.uns['qc_summary_table']. + """ + + # Check that required columns exist in adata.obs + check_annotation( + adata, + annotations=stat_columns_list, + should_exist=True) + + # compute summary statistics for the specified columns + def compute_stats(df): + stat_vals = [] + for col_name in stat_columns_list: + # Ensure the column is numeric + if not pd.api.types.is_numeric_dtype(df[col_name]): + raise TypeError(f"Column '{col_name}' must be numeric to compute statistics.") + # Compute median and MAD (median absolute deviation) + median = df[col_name].median() + mad = median_abs_deviation(df[col_name], nan_policy='omit') + # Collect statistics for this column + col_stats = [ + col_name, + df[col_name].mean(), + median, + median + n_mad * mad, + median - n_mad * mad, + df[col_name].quantile(upper_quantile), + df[col_name].quantile(lower_quantile) + ] + stat_vals.append(col_stats) + # Return DataFrame with statistics for all columns + return pd.DataFrame( + stat_vals, + columns=[ + "metric_name", "mean", "median", + "upper_mad", "lower_mad", + "upper_quantile", "lower_quantile" + ] + ) + + obs_df = adata.obs + summary_table = pd.DataFrame() + # If no sample_column, compute stats for all data + if sample_column is None: + stat_df = compute_stats(obs_df) + stat_df["Sample"] = "All" + summary_table = stat_df + else: + # Otherwise, compute stats for each sample group + samples_list = pd.unique(obs_df[sample_column]) + for current_sample in samples_list: + sample_df = obs_df[obs_df[sample_column] == current_sample] + stat_df = compute_stats(sample_df) + stat_df["Sample"] = current_sample + summary_table = pd.concat([summary_table, stat_df]) + # Reset index and store in adata.uns + summary_table = summary_table.reset_index(drop=True) + adata.uns["qc_summary_table"] = summary_table \ No newline at end of file diff --git a/tests/test_transformations/test_get_qc_summary_table.py b/tests/test_transformations/test_get_qc_summary_table.py new file mode 100644 index 00000000..6ea91a5f --- /dev/null +++ b/tests/test_transformations/test_get_qc_summary_table.py @@ -0,0 +1,83 @@ +import unittest +import numpy as np +import pandas as pd +import scanpy as sc +from anndata import AnnData +from spac.transformations import add_qc_metrics +from spac.transformations import get_qc_summary_table + +class TestGetQCSummaryTable(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Set a random seed for reproducibility + np.random.seed(42) + + # Create a small AnnData object for testing + def create_test_adata(self): + X = np.array([ + [1, 0, 3, 0], + [0, 2, 0, 4], + [5, 0, 0, 6] + ]) + var_names = ["MT-CO1", "MT-CO2", "GeneA", "GeneB"] + obs_names = ["cell1", "cell2", "cell3"] + adata = AnnData(X=X) + adata.var_names = var_names + adata.obs_names = obs_names + # Compute QC metrics using the provided function + add_qc_metrics(adata) + return adata + + # Test that the summary table is created and has the correct structure + def test_qc_summary_table_basic(self): + adata = self.create_test_adata() + get_qc_summary_table(adata) + self.assertIn("qc_summary_table", adata.uns) + summary = adata.uns["qc_summary_table"] + self.assertTrue(isinstance(summary, pd.DataFrame)) + # Check that all expected columns are present + self.assertIn("mean", summary.columns) + self.assertIn("median", summary.columns) + self.assertIn("upper_mad", summary.columns) + self.assertIn("lower_mad", summary.columns) + self.assertIn("upper_quantile", summary.columns) + self.assertIn("lower_quantile", summary.columns) + self.assertIn("Sample", summary.columns) + # Check that the correct metrics are summarized + self.assertEqual(set(summary["metric_name"]), {"nFeature", "nCount", "percent.mt"}) + # Check that the sample label is correct when not grouping + self.assertEqual(summary["Sample"].iloc[0], "All") + + # Test that a TypeError is raised if a non-numeric column is included + def test_qc_summary_table_non_numeric(self): + adata = self.create_test_adata() + adata.obs["non_numeric"] = ["a", "b", "c"] + with self.assertRaises(TypeError): + get_qc_summary_table(adata, stat_columns_list=["nFeature", "non_numeric"]) + + # Test that summary statistics are computed correctly for nFeature and nCount + def test_qc_summary_table_statistics(self): + adata = self.create_test_adata() + get_qc_summary_table(adata) + summary = adata.uns["qc_summary_table"] + # Check mean, median, quantiles for nFeature (all values are 2) + nfeature_row = summary[summary["metric_name"] == "nFeature"].iloc[0] + self.assertEqual(nfeature_row["mean"], 2) + self.assertEqual(nfeature_row["median"], 2) + self.assertEqual(nfeature_row["upper_mad"], 2) + self.assertEqual(nfeature_row["lower_mad"], 2) + self.assertEqual(nfeature_row["upper_quantile"], 2) + self.assertEqual(nfeature_row["lower_quantile"], 2) + # Check mean, median, quantiles for nCount + ncount_row = summary[summary["metric_name"] == "nCount"].iloc[0] + expected_mean = np.mean([4, 6, 11]) + expected_median = np.median([4, 6, 11]) + expected_upper = np.percentile([4, 6, 11], 95) + expected_lower = np.percentile([4, 6, 11], 5) + self.assertAlmostEqual(ncount_row["mean"], expected_mean) + self.assertAlmostEqual(ncount_row["median"], expected_median) + self.assertAlmostEqual(ncount_row["upper_quantile"], expected_upper) + self.assertAlmostEqual(ncount_row["lower_quantile"], expected_lower) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 5e03dc23b5deaadb853fd26f327f643d7e19ad12 Mon Sep 17 00:00:00 2001 From: Andrei Bombin Date: Fri, 11 Jul 2025 14:07:33 -0400 Subject: [PATCH 006/102] refactor(get_qc_summary_table): adjust code style to adhere to spac guidlines closer --- src/spac/transformations.py | 73 ++++++++-------- src/spac/utils.py | 83 +++++++++++++++++++ .../test_get_qc_summary_table.py | 60 +++++++++++--- 3 files changed, 166 insertions(+), 50 deletions(-) diff --git a/src/spac/transformations.py b/src/spac/transformations.py index 09f4f7bc..2022ace3 100644 --- a/src/spac/transformations.py +++ b/src/spac/transformations.py @@ -16,8 +16,9 @@ import multiprocessing import parmap from spac.utag_functions import utag -from scipy.stats import median_abs_deviation from anndata import AnnData +from spac.utils import compute_summary_qc_stats +from typing import List, Optional # Configure logging logging.basicConfig(level=logging.INFO, @@ -1385,12 +1386,13 @@ def get_qc_summary_table( n_mad: int = 5, upper_quantile: float = 0.95, lower_quantile: float = 0.05, - stat_columns_list: list = ["nFeature", "nCount", "percent.mt"], + stat_columns_list: Optional[List[str]] = None, sample_column: str = None ) -> None: """ Compute summary statistics for quality control metrics in an AnnData object and store the result in adata.uns['qc_summary_table']. + If QC columns are not in the adata.obs, run add_qc_metrics first. Parameters: adata (AnnData): The AnnData object containing the data. @@ -1398,65 +1400,62 @@ def get_qc_summary_table( upper_quantile (float): Upper quantile to compute (e.g., 0.95). lower_quantile (float): Lower quantile to compute (e.g., 0.05). stat_columns_list (list): List of column names to compute statistics for. + If None, defaults to ['nFeature', 'nCount', 'percent.mt']. sample_column (str, optional): Column name to group by sample. If None, computes for all data. Returns: None. The summary table is stored in adata.uns['qc_summary_table']. """ + # if not provided select default stat columns + if stat_columns_list is None: + stat_columns_list = ['nFeature', 'nCount', 'percent.mt'] # Check that required columns exist in adata.obs check_annotation( adata, annotations=stat_columns_list, should_exist=True) - - # compute summary statistics for the specified columns - def compute_stats(df): - stat_vals = [] - for col_name in stat_columns_list: - # Ensure the column is numeric - if not pd.api.types.is_numeric_dtype(df[col_name]): - raise TypeError(f"Column '{col_name}' must be numeric to compute statistics.") - # Compute median and MAD (median absolute deviation) - median = df[col_name].median() - mad = median_abs_deviation(df[col_name], nan_policy='omit') - # Collect statistics for this column - col_stats = [ - col_name, - df[col_name].mean(), - median, - median + n_mad * mad, - median - n_mad * mad, - df[col_name].quantile(upper_quantile), - df[col_name].quantile(lower_quantile) - ] - stat_vals.append(col_stats) - # Return DataFrame with statistics for all columns - return pd.DataFrame( - stat_vals, - columns=[ - "metric_name", "mean", "median", - "upper_mad", "lower_mad", - "upper_quantile", "lower_quantile" - ] - ) + + # check grouping column + if sample_column is not None: + check_annotation(adata, annotations=[sample_column], should_exist=True) + + # validate numerical parameters input + if not 0 <= upper_quantile <= 1: + raise ValueError(f'Parameter "upper_quantile" must be between 0 and 1, got "{upper_quantile}"' + ) + if not 0 <= lower_quantile <= 1: + raise ValueError(f'Parameter "lower_quantile" must be between 0 and 1, got "{lower_quantile}"' + ) + if n_mad < 0: + raise ValueError(f'Parameter "n_mad" must be non-negative, got "{n_mad}"') obs_df = adata.obs summary_table = pd.DataFrame() # If no sample_column, compute stats for all data if sample_column is None: - stat_df = compute_stats(obs_df) + stat_df = compute_summary_qc_stats(df=obs_df, + n_mad=n_mad, + upper_quantile=upper_quantile, + lower_quantile=lower_quantile, + stat_columns_list=stat_columns_list) stat_df["Sample"] = "All" summary_table = stat_df else: # Otherwise, compute stats for each sample group samples_list = pd.unique(obs_df[sample_column]) + stat_dfs = [] for current_sample in samples_list: - sample_df = obs_df[obs_df[sample_column] == current_sample] - stat_df = compute_stats(sample_df) + sample_df = obs_df[obs_df[sample_column] == current_sample].copy() + stat_df = compute_summary_qc_stats(df=sample_df, + n_mad=n_mad, + upper_quantile=upper_quantile, + lower_quantile=lower_quantile, + stat_columns_list=stat_columns_list) stat_df["Sample"] = current_sample - summary_table = pd.concat([summary_table, stat_df]) + stat_dfs.append(stat_df) + summary_table = pd.concat(stat_dfs, ignore_index=True) # Reset index and store in adata.uns summary_table = summary_table.reset_index(drop=True) adata.uns["qc_summary_table"] = summary_table \ No newline at end of file diff --git a/src/spac/utils.py b/src/spac/utils.py index f3506a20..21a23223 100644 --- a/src/spac/utils.py +++ b/src/spac/utils.py @@ -7,6 +7,8 @@ import logging import warnings import numbers +from scipy.stats import median_abs_deviation +from typing import List, Optional # Configure logging logging.basicConfig(level=logging.INFO, @@ -1190,3 +1192,84 @@ def compute_metrics(data): return metrics return metrics + +# compute summary statistics for the specified columns +def compute_summary_qc_stats( + df: pd.DataFrame, + n_mad: int = 5, + upper_quantile: float = 0.95, + lower_quantile: float = 0.05, + stat_columns_list: Optional[List[str]] = None + ) -> pd.DataFrame: + + """ + Compute summary quality control statistics for specified columns in a dataset. + + For each column in stat_columns_list, this function calculates: + - Mean + - Median + - Upper and lower thresholds based on median ± n_mad * MAD + (median absolute deviation) + - Upper and lower quantiles + + Parameters + ---------- + df : pd.DataFrame + Input DataFrame containing the data. + n_mad : int, optional + Number of MADs to use for upper and lower thresholds (default is 5). + upper_quantile : float, optional + Upper quantile to compute (default is 0.95). + lower_quantile : float, optional + Lower quantile to compute (default is 0.05). + stat_columns_list : list of str, optional + List of column names to compute statistics for. Columns must be numeric. + + Returns + ------- + pd.DataFrame + DataFrame with summary statistics for each specified column. + Columns: ["metric_name", "mean", "median", "upper_mad", "lower_mad", + "upper_quantile", "lower_quantile"] + + Raises + ------ + TypeError + If any column in stat_columns_list is not numeric or all values are NaN. + """ + stat_vals = [] + for col_name in stat_columns_list: + # Ensure the column is numeric + if not pd.api.types.is_numeric_dtype(df[col_name]): + raise TypeError( + f'Column "{col_name}" must be numeric to compute statistics.' + ) + # Check for all-NaN column + if df[col_name].isna().all(): + raise TypeError( + f'Column "{col_name}" must be numeric to compute statistics. ' + 'All values are NaN.' + ) + # Compute median and MAD (median absolute deviation) + median = df[col_name].median() + mad = median_abs_deviation(df[col_name], nan_policy='omit') + # Collect statistics for this column + col_stats = [ + col_name, + df[col_name].mean(), + median, + median + n_mad * mad, + median - n_mad * mad, + df[col_name].quantile(upper_quantile), + df[col_name].quantile(lower_quantile) + ] + stat_vals.append(col_stats) + # Return DataFrame with statistics for all columns + return pd.DataFrame( + stat_vals, + columns=[ + "metric_name", "mean", "median", + "upper_mad", "lower_mad", + "upper_quantile", "lower_quantile" + ] + ) \ No newline at end of file diff --git a/tests/test_transformations/test_get_qc_summary_table.py b/tests/test_transformations/test_get_qc_summary_table.py index 6ea91a5f..5eddd72b 100644 --- a/tests/test_transformations/test_get_qc_summary_table.py +++ b/tests/test_transformations/test_get_qc_summary_table.py @@ -32,8 +32,8 @@ def create_test_adata(self): def test_qc_summary_table_basic(self): adata = self.create_test_adata() get_qc_summary_table(adata) - self.assertIn("qc_summary_table", adata.uns) summary = adata.uns["qc_summary_table"] + self.assertIn("qc_summary_table", adata.uns) self.assertTrue(isinstance(summary, pd.DataFrame)) # Check that all expected columns are present self.assertIn("mean", summary.columns) @@ -44,7 +44,8 @@ def test_qc_summary_table_basic(self): self.assertIn("lower_quantile", summary.columns) self.assertIn("Sample", summary.columns) # Check that the correct metrics are summarized - self.assertEqual(set(summary["metric_name"]), {"nFeature", "nCount", "percent.mt"}) + self.assertEqual(set(summary["metric_name"]), + {"nFeature", "nCount", "percent.mt"}) # Check that the sample label is correct when not grouping self.assertEqual(summary["Sample"].iloc[0], "All") @@ -52,8 +53,11 @@ def test_qc_summary_table_basic(self): def test_qc_summary_table_non_numeric(self): adata = self.create_test_adata() adata.obs["non_numeric"] = ["a", "b", "c"] - with self.assertRaises(TypeError): - get_qc_summary_table(adata, stat_columns_list=["nFeature", "non_numeric"]) + with self.assertRaises(TypeError) as exc_info: + get_qc_summary_table(adata, + stat_columns_list=["nFeature", "non_numeric"]) + expected_msg = 'Column "non_numeric" must be numeric to compute statistics.' + self.assertEqual(str(exc_info.exception), expected_msg) # Test that summary statistics are computed correctly for nFeature and nCount def test_qc_summary_table_statistics(self): @@ -68,16 +72,46 @@ def test_qc_summary_table_statistics(self): self.assertEqual(nfeature_row["lower_mad"], 2) self.assertEqual(nfeature_row["upper_quantile"], 2) self.assertEqual(nfeature_row["lower_quantile"], 2) - # Check mean, median, quantiles for nCount + # Check nCount statistics + # nCount per cell = [4, 6, 11] -> + # mean 7.0, median 6.0, 95th pct 10.5, 5th pct 4.2 ncount_row = summary[summary["metric_name"] == "nCount"].iloc[0] - expected_mean = np.mean([4, 6, 11]) - expected_median = np.median([4, 6, 11]) - expected_upper = np.percentile([4, 6, 11], 95) - expected_lower = np.percentile([4, 6, 11], 5) - self.assertAlmostEqual(ncount_row["mean"], expected_mean) - self.assertAlmostEqual(ncount_row["median"], expected_median) - self.assertAlmostEqual(ncount_row["upper_quantile"], expected_upper) - self.assertAlmostEqual(ncount_row["lower_quantile"], expected_lower) + self.assertAlmostEqual(ncount_row["mean"], 7.0) + self.assertAlmostEqual(ncount_row["median"], 6.0) + self.assertAlmostEqual(ncount_row["upper_quantile"], 10.5) + self.assertAlmostEqual(ncount_row["lower_quantile"], 4.2) + + # Test that summary statistics is computed correctly with sample_column grouping + def test_qc_summary_table_grouping(self): + adata = self.create_test_adata() + get_qc_summary_table(adata) + # Add a sample column with two groups + adata.obs["batch"] = ["A", "A", "B"] + get_qc_summary_table(adata, sample_column="batch") + summary = adata.uns["qc_summary_table"] + # There should be two groups: A and B + self.assertEqual(set(summary["Sample"]), {"A", "B"}) + # For group A (cells 0 and 1): nCount = [4, 6] + group_a = summary[(summary["Sample"] == "A") & (summary["metric_name"] == "nCount")].iloc[0] + self.assertAlmostEqual(group_a["mean"], 5.0) + self.assertAlmostEqual(group_a["median"], 5.0) + # For group B (cell 2): nCount = [11] + group_b = summary[(summary["Sample"] == "B") & + (summary["metric_name"] == "nCount")].iloc[0] + self.assertAlmostEqual(group_b["mean"], 11.0) + self.assertAlmostEqual(group_b["median"], 11.0) + + # Test that all-NaN columns are handled gracefully + def test_qc_summary_table_all_nan_column(self): + adata = self.create_test_adata() + adata.obs["all_nan"] = [np.nan, np.nan, np.nan] + with self.assertRaises(TypeError) as exc_info: + get_qc_summary_table(adata, stat_columns_list=["all_nan"]) + expected_msg = ( + 'Column "all_nan" must be numeric to compute statistics. ' + 'All values are NaN.' + ) + self.assertEqual(str(exc_info.exception), expected_msg) if __name__ == "__main__": unittest.main() \ No newline at end of file From 239f981fb7ad12f5fcb431146125e7df7f28a703 Mon Sep 17 00:00:00 2001 From: Andrei Bombin Date: Fri, 11 Jul 2025 14:20:09 -0400 Subject: [PATCH 007/102] Update src/spac/utils.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/spac/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spac/utils.py b/src/spac/utils.py index 21a23223..86665650 100644 --- a/src/spac/utils.py +++ b/src/spac/utils.py @@ -1199,7 +1199,7 @@ def compute_summary_qc_stats( n_mad: int = 5, upper_quantile: float = 0.95, lower_quantile: float = 0.05, - stat_columns_list: Optional[List[str]] = None + stat_columns_list: List[str] ) -> pd.DataFrame: """ From df46aca462eaa40cba2be8cd29e40b1d39302ff3 Mon Sep 17 00:00:00 2001 From: Andrei Bombin Date: Fri, 11 Jul 2025 14:24:10 -0400 Subject: [PATCH 008/102] make sure that compute_summary_qc_stats has a default input for stat_columns_list --- src/spac/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spac/utils.py b/src/spac/utils.py index 86665650..10a31ab2 100644 --- a/src/spac/utils.py +++ b/src/spac/utils.py @@ -1199,7 +1199,7 @@ def compute_summary_qc_stats( n_mad: int = 5, upper_quantile: float = 0.95, lower_quantile: float = 0.05, - stat_columns_list: List[str] + stat_columns_list: List[str] = ['nFeature', 'nCount', 'percent.mt'] ) -> pd.DataFrame: """ From d5061c43d31c20338576c58d207619f9ae789143 Mon Sep 17 00:00:00 2001 From: Andrei Bombin Date: Tue, 15 Jul 2025 14:25:01 -0400 Subject: [PATCH 009/102] refactor(get_qc_summary_table): refactor quality control summary statistics function and tests based on the PR review --- src/spac/transformations.py | 6 ++ .../test_get_qc_summary_table.py | 40 +++------- .../test_compute_summary_qc_stats.py | 74 +++++++++++++++++++ 3 files changed, 89 insertions(+), 31 deletions(-) create mode 100644 tests/test_utils/test_compute_summary_qc_stats.py diff --git a/src/spac/transformations.py b/src/spac/transformations.py index 2022ace3..228c55f1 100644 --- a/src/spac/transformations.py +++ b/src/spac/transformations.py @@ -1417,6 +1417,12 @@ def get_qc_summary_table( annotations=stat_columns_list, should_exist=True) + # check that stat_column_list is not empty + if not stat_columns_list: # catches [], (), None + raise ValueError( + 'Parameter "stat_columns_list" must contain at least one column name.' + ) + # check grouping column if sample_column is not None: check_annotation(adata, annotations=[sample_column], should_exist=True) diff --git a/tests/test_transformations/test_get_qc_summary_table.py b/tests/test_transformations/test_get_qc_summary_table.py index 5eddd72b..2894a3af 100644 --- a/tests/test_transformations/test_get_qc_summary_table.py +++ b/tests/test_transformations/test_get_qc_summary_table.py @@ -59,29 +59,8 @@ def test_qc_summary_table_non_numeric(self): expected_msg = 'Column "non_numeric" must be numeric to compute statistics.' self.assertEqual(str(exc_info.exception), expected_msg) - # Test that summary statistics are computed correctly for nFeature and nCount - def test_qc_summary_table_statistics(self): - adata = self.create_test_adata() - get_qc_summary_table(adata) - summary = adata.uns["qc_summary_table"] - # Check mean, median, quantiles for nFeature (all values are 2) - nfeature_row = summary[summary["metric_name"] == "nFeature"].iloc[0] - self.assertEqual(nfeature_row["mean"], 2) - self.assertEqual(nfeature_row["median"], 2) - self.assertEqual(nfeature_row["upper_mad"], 2) - self.assertEqual(nfeature_row["lower_mad"], 2) - self.assertEqual(nfeature_row["upper_quantile"], 2) - self.assertEqual(nfeature_row["lower_quantile"], 2) - # Check nCount statistics - # nCount per cell = [4, 6, 11] -> - # mean 7.0, median 6.0, 95th pct 10.5, 5th pct 4.2 - ncount_row = summary[summary["metric_name"] == "nCount"].iloc[0] - self.assertAlmostEqual(ncount_row["mean"], 7.0) - self.assertAlmostEqual(ncount_row["median"], 6.0) - self.assertAlmostEqual(ncount_row["upper_quantile"], 10.5) - self.assertAlmostEqual(ncount_row["lower_quantile"], 4.2) - - # Test that summary statistics is computed correctly with sample_column grouping + # Test that summary statistics is computed correctly with + # sample_column grouping def test_qc_summary_table_grouping(self): adata = self.create_test_adata() get_qc_summary_table(adata) @@ -92,7 +71,8 @@ def test_qc_summary_table_grouping(self): # There should be two groups: A and B self.assertEqual(set(summary["Sample"]), {"A", "B"}) # For group A (cells 0 and 1): nCount = [4, 6] - group_a = summary[(summary["Sample"] == "A") & (summary["metric_name"] == "nCount")].iloc[0] + group_a = summary[(summary["Sample"] == "A") & + (summary["metric_name"] == "nCount")].iloc[0] self.assertAlmostEqual(group_a["mean"], 5.0) self.assertAlmostEqual(group_a["median"], 5.0) # For group B (cell 2): nCount = [11] @@ -101,15 +81,13 @@ def test_qc_summary_table_grouping(self): self.assertAlmostEqual(group_b["mean"], 11.0) self.assertAlmostEqual(group_b["median"], 11.0) - # Test that all-NaN columns are handled gracefully - def test_qc_summary_table_all_nan_column(self): + #Test that ValueError is raised if stat_columns_list is empty + def test_qc_summary_table_empty_stat_columns_list(self): adata = self.create_test_adata() - adata.obs["all_nan"] = [np.nan, np.nan, np.nan] - with self.assertRaises(TypeError) as exc_info: - get_qc_summary_table(adata, stat_columns_list=["all_nan"]) + with self.assertRaises(ValueError) as exc_info: + get_qc_summary_table(adata, stat_columns_list=[]) expected_msg = ( - 'Column "all_nan" must be numeric to compute statistics. ' - 'All values are NaN.' + 'Parameter "stat_columns_list" must contain at least one column name.' ) self.assertEqual(str(exc_info.exception), expected_msg) diff --git a/tests/test_utils/test_compute_summary_qc_stats.py b/tests/test_utils/test_compute_summary_qc_stats.py new file mode 100644 index 00000000..9ae628d4 --- /dev/null +++ b/tests/test_utils/test_compute_summary_qc_stats.py @@ -0,0 +1,74 @@ +import unittest +import numpy as np +import pandas as pd +from spac.utils import compute_summary_qc_stats + +class TestComputeSummaryQCStats(unittest.TestCase): + def setUp(self): + # Create a simple DataFrame for testing + self.df = pd.DataFrame({ + "nFeature": [2, 2, 2], + "nCount": [4, 6, 11], + "percent.mt": [25.0, 33.33333333333333, 45.45454545454545], + "all_nan": [np.nan, np.nan, np.nan], + "non_numeric": ["a", "b", "c"] + }) + + # Test that summary statistics are computed correctly for nFeature + def test_basic_statistics(self): + result = compute_summary_qc_stats(self.df, + stat_columns_list=["nFeature"]) + row = result.iloc[0] + self.assertEqual(row["mean"], 2) + self.assertEqual(row["median"], 2) + self.assertEqual(row["upper_mad"], 2) + self.assertEqual(row["lower_mad"], 2) + self.assertEqual(row["upper_quantile"], 2) + self.assertEqual(row["lower_quantile"], 2) + + # Test that summary statistics are computed correctly for nCount + def test_ncount_statistics(self): + # nCount: [4, 6, 11] -> mean 7.0, median 6.0, 95th pct 10.5, 5th pct 4.2 + result = compute_summary_qc_stats(self.df, + stat_columns_list=["nCount"]) + row = result.iloc[0] + self.assertAlmostEqual(row["mean"], 7.0) + self.assertAlmostEqual(row["median"], 6.0) + self.assertAlmostEqual(row["upper_quantile"], 10.5) + self.assertAlmostEqual(row["lower_quantile"], 4.2) + + # Test that summary statistics are computed correctly for percent.mt + def test_percent_mt_statistics(self): + # percent.mt: [25.0, 33.33333333333333, 45.45454545454545] -> + # mean 34.59596, median 33.33333, upper_quantile 44.24242, + # lower_quantile 25.83333 + result = compute_summary_qc_stats(self.df, + stat_columns_list=["percent.mt"]) + row = result.iloc[0] + self.assertAlmostEqual(row["mean"], 34.59596, places=5) + self.assertAlmostEqual(row["median"], 33.33333, places=5) + self.assertAlmostEqual(row["upper_quantile"], 44.24242, places=5) + self.assertAlmostEqual(row["lower_quantile"], 25.83333, places=5) + + # Test that a TypeError is raised if a non-numeric column is included + def test_non_numeric_column_raises(self): + with self.assertRaises(TypeError) as exc_info: + compute_summary_qc_stats(self.df, + stat_columns_list=["non_numeric"]) + expected_msg = ( + 'Column "non_numeric" must be numeric to compute statistics.' + ) + self.assertEqual(str(exc_info.exception), expected_msg) + + # Test that all-NaN columns are handled gracefully + def test_all_nan_column_raises(self): + with self.assertRaises(TypeError) as exc_info: + compute_summary_qc_stats(self.df, stat_columns_list=["all_nan"]) + expected_msg = ( + 'Column "all_nan" must be numeric to compute statistics. ' + 'All values are NaN.' + ) + self.assertEqual(str(exc_info.exception), expected_msg) + +if __name__ == "__main__": + unittest.main() From b960684f3e1887330f91ad307cc36b467d68bea3 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:31:21 -0400 Subject: [PATCH 010/102] feat(template_utils): add template_utils and unit tests --- src/spac/templates/template_utils.py | 359 +++++++++++++++++++++++++ tests/templates/test_template_utils.py | 297 ++++++++++++++++++++ 2 files changed, 656 insertions(+) create mode 100644 src/spac/templates/template_utils.py create mode 100644 tests/templates/test_template_utils.py diff --git a/src/spac/templates/template_utils.py b/src/spac/templates/template_utils.py new file mode 100644 index 00000000..aeccb3ae --- /dev/null +++ b/src/spac/templates/template_utils.py @@ -0,0 +1,359 @@ +from pathlib import Path +import pickle +from typing import Any, Dict, Union, Optional, List +import json +import anndata as ad + + +def load_input(file_path: Union[str, Path]): + """ + Load input data from either h5ad or pickle file. + + Parameters + ---------- + file_path : str or Path + Path to input file (h5ad or pickle) + + Returns + ------- + Loaded data object (typically AnnData) + """ + path = Path(file_path) + + if not path.exists(): + raise FileNotFoundError(f"Input file not found: {file_path}") + + # Check file extension + suffix = path.suffix.lower() + + if suffix in ['.h5ad', '.h5']: + # Load h5ad file + try: + import anndata as ad + return ad.read_h5ad(path) + except ImportError: + raise ImportError( + "anndata package required to read h5ad files" + ) + except Exception as e: + raise ValueError(f"Error reading h5ad file: {e}") + + elif suffix in ['.pickle', '.pkl', '.p']: + # Load pickle file + with path.open('rb') as fh: + return pickle.load(fh) + + else: + # Try to detect file type by content + try: + # First try h5ad + import anndata as ad + return ad.read_h5ad(path) + except Exception: + # Fall back to pickle + try: + with path.open('rb') as fh: + return pickle.load(fh) + except Exception as e: + raise ValueError( + f"Unable to load file '{file_path}'. " + f"Supported formats: h5ad, pickle. Error: {e}" + ) + + +def save_outputs( + outputs: Dict[str, Any], + output_dir: Union[str, Path] = "." +) -> Dict[str, str]: + """ + Save multiple outputs to files and return a dict {filename: absolute_path}. + (Always a dict, even if just one file.) + + Parameters + ---------- + outputs : dict + Dictionary where: + - key: filename (with extension) + - value: object to save + output_dir : str or Path + Directory to save files + + Returns + ------- + dict + Dictionary of saved file paths + + Example + ------- + >>> outputs = { + ... "adata.pickle": adata, # Preferred format + ... "results.csv": results_df, + ... "adata.h5ad": adata # Still supported + ... } + >>> saved = save_outputs(outputs, "results/") + """ + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + saved_files = {} + + for filename, obj in outputs.items(): + filepath = output_dir / filename + + # Save based on file extension + if filename.endswith('.csv'): + obj.to_csv(filepath, index=False) + elif filename.endswith('.h5ad'): + # Still support h5ad, but not the default + if type(obj) is not ad.AnnData: + raise TypeError( + f"Object for '{str(filename)}' must be AnnData, got {type(obj)}" + ) + print(f"Saving AnnData to {str(filepath)}") + print(obj) + obj.write_h5ad(str(filepath)) + print(f"Saved AnnData to {str(filepath)}") + elif filename.endswith(('.pickle', '.pkl', '.p')): + with open(filepath, 'wb') as f: + pickle.dump(obj, f) + elif hasattr(obj, "savefig"): + obj.savefig(filepath.with_suffix('.png')) + filepath = filepath.with_suffix('.png') + else: + # Default to pickle + filepath = filepath.with_suffix('.pickle') + with open(filepath, 'wb') as f: + pickle.dump(obj, f) + + # filepath = filepath.resolve() + print(type(filepath)) + print(type(filename)) + saved_files[str(filename)] = str(filepath) + print(f"Saved: {filepath}") + + return saved_files + + +def parse_params( + json_input: Union[str, Path, Dict[str, Any]] +) -> Dict[str, Any]: + """ + Parse parameters from JSON file, string, or dict. + + Parameters + ---------- + json_input : str, Path, or dict + JSON file path, JSON string, or dictionary + + Returns + ------- + dict + Parsed parameters + """ + if isinstance(json_input, dict): + return json_input + + if isinstance(json_input, (str, Path)): + path = Path(json_input) + + # Check if it's a file path + if path.exists() or str(json_input).endswith('.json'): + with open(path, 'r') as file: + return json.load(file) + else: + # It's a JSON string + return json.loads(str(json_input)) + + raise TypeError( + "json_input must be dict, JSON string, or path to JSON file" + ) + + +def text_to_value( + var: Any, + default_none_text: str = "None", + value_to_convert_to: Any = None, + to_float: bool = False, + to_int: bool = False, + param_name: str = '' +): + """ + Converts a string to a specified value or type. Handles conversion to + float or integer and provides a default value if the input string + matches a specified 'None' text. + + Parameters + ---------- + var : str + The input string to be converted. + default_none_text : str, optional + The string that represents a 'None' value. If `var` matches this + string, it will be converted to `value_to_convert_to`. + Default is "None". + value_to_convert_to : any, optional + The value to assign to `var` if it matches `default_none_text` or + is an empty string. Default is None. + to_float : bool, optional + If True, attempt to convert `var` to a float. Default is False. + to_int : bool, optional + If True, attempt to convert `var` to an integer. Default is False. + param_name : str, optional + The name of the parameter, used in error messages for conversion + failures. Default is ''. + + Returns + ------- + any + The converted value, which may be the original string, a float, + an integer, or the specified `value_to_convert_to`. + + Raises + ------ + ValueError + If `to_float` or `to_int` is set to True and conversion fails. + + Notes + ----- + - If both `to_float` and `to_int` are set to True, the function will + prioritize conversion to float. + - If the string `var` matches `default_none_text` or is an empty + string, `value_to_convert_to` is returned. + + Examples + -------- + Convert a string representing a float: + + >>> text_to_value("3.14", to_float=True) + 3.14 + + Handle a 'None' string: + + >>> text_to_value("None", value_to_convert_to=None) + None + + Convert a string to an integer: + + >>> text_to_value("42", to_int=True) + 42 + + Handle invalid conversion: + + >>> text_to_value("abc", to_int=True, param_name="test_param") + Error: can't convert test_param to integer. Received:"abc" + 'abc' + """ + # Handle non-string inputs + if not isinstance(var, str): + var = str(var) + + none_condition = ( + var.lower().strip() == default_none_text.lower().strip() or + var.strip() == '' + ) + + if none_condition: + var = value_to_convert_to + + elif to_float: + try: + var = float(var) + except ValueError: + error_msg = ( + f'Error: can\'t convert {param_name} to float. ' + f'Received:"{var}"' + ) + raise ValueError(error_msg) + + elif to_int: + try: + var = int(var) + except ValueError: + error_msg = ( + f'Error: can\'t convert {param_name} to integer. ' + f'Received:"{var}"' + ) + raise ValueError(error_msg) + + return var + + +def convert_to_floats(text_list: List[Any]) -> List[float]: + """ + Convert list of text values to floats. + + Parameters + ---------- + text_list : list + List of values to convert + + Returns + ------- + list + List of float values + + Raises + ------ + ValueError + If any value cannot be converted to float + """ + float_list = [] + for value in text_list: + try: + float_list.append(float(value)) + except ValueError: + msg = f"Failed to convert the radius: '{value}' to float." + raise ValueError(msg) + return float_list + + +def convert_pickle_to_h5ad( + pickle_path: Union[str, Path], + h5ad_path: Optional[Union[str, Path]] = None +) -> str: + """ + Convert a pickle file containing AnnData to h5ad format. + + Parameters + ---------- + pickle_path : str or Path + Path to input pickle file + h5ad_path : str or Path, optional + Path for output h5ad file. If None, uses same name with .h5ad + extension + + Returns + ------- + str + Path to saved h5ad file + """ + pickle_path = Path(pickle_path) + + if not pickle_path.exists(): + raise FileNotFoundError(f"Pickle file not found: {pickle_path}") + + # Load from pickle + with pickle_path.open('rb') as fh: + adata = pickle.load(fh) + + # Check if it's AnnData + try: + import anndata as ad + if not isinstance(adata, ad.AnnData): + raise TypeError( + f"Loaded object is not AnnData, got {type(adata)}" + ) + except ImportError: + raise ImportError( + "anndata package required for conversion to h5ad" + ) + + # Determine output path + if h5ad_path is None: + h5ad_path = pickle_path.with_suffix('.h5ad') + else: + h5ad_path = Path(h5ad_path) + + # Save as h5ad + adata.write_h5ad(h5ad_path) + + return str(h5ad_path) \ No newline at end of file diff --git a/tests/templates/test_template_utils.py b/tests/templates/test_template_utils.py new file mode 100644 index 00000000..d6b424b0 --- /dev/null +++ b/tests/templates/test_template_utils.py @@ -0,0 +1,297 @@ +# tests/utils/test_template_utils.py +"""Unit tests for template utilities.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.template_utils import ( + load_input, + save_outputs, + text_to_value, + convert_pickle_to_h5ad, + convert_to_floats +) + + +def mock_adata(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "cell_type": ["TypeA", "TypeB"] * (n_cells // 2) + }) + x_mat = rng.normal(size=(n_cells, 2)) + adata = ad.AnnData(X=x_mat, obs=obs) + return adata + + +def mock_dataframe(n_rows: int = 5) -> pd.DataFrame: + """Return a minimal DataFrame for fast tests.""" + return pd.DataFrame({ + "col1": range(n_rows), + "col2": [f"value_{i}" for i in range(n_rows)] + }) + + +class TestTemplateUtils(unittest.TestCase): + """Unit tests for template utility functions.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.test_adata = mock_adata() + self.test_df = mock_dataframe() + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering all input/output scenarios.""" + # Suppress warnings for cleaner test output + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Load h5ad file + h5ad_path = os.path.join(self.tmp_dir.name, "test.h5ad") + self.test_adata.write_h5ad(h5ad_path) + loaded_h5ad = load_input(h5ad_path) + self.assertEqual(loaded_h5ad.n_obs, 10) + self.assertIn("cell_type", loaded_h5ad.obs.columns) + + # Test 2: Load pickle file + pickle_path = os.path.join(self.tmp_dir.name, "test.pickle") + with open(pickle_path, "wb") as f: + pickle.dump(self.test_adata, f) + loaded_pickle = load_input(pickle_path) + self.assertEqual(loaded_pickle.n_obs, 10) + + # Test 3: Load .pkl extension + pkl_path = os.path.join(self.tmp_dir.name, "test.pkl") + with open(pkl_path, "wb") as f: + pickle.dump(self.test_adata, f) + loaded_pkl = load_input(pkl_path) + self.assertEqual(loaded_pkl.n_obs, 10) + + # Test 4: Load .p extension + p_path = os.path.join(self.tmp_dir.name, "test.p") + with open(p_path, "wb") as f: + pickle.dump(self.test_adata, f) + loaded_p = load_input(p_path) + self.assertEqual(loaded_p.n_obs, 10) + + # Test 5: Save outputs - multiple formats + outputs = { + "result.pickle": self.test_adata, # Now preferred format + "data.csv": self.test_df, + "adata.pkl": self.test_adata, + "adata.h5ad": self.test_adata, # Still supported + "other_data": {"key": "value"} # Defaults to pickle + } + saved_files = save_outputs(outputs, self.tmp_dir.name) + + # Verify all files were saved + self.assertEqual(len(saved_files), 5) + for filename, filepath in saved_files.items(): + self.assertTrue(os.path.exists(filepath)) + self.assertIn(filename, saved_files) + + # Verify CSV content + csv_path = saved_files["data.csv"] + loaded_df = pd.read_csv(csv_path) + self.assertEqual(len(loaded_df), 5) + self.assertIn("col1", loaded_df.columns) + + # Test 6: Convert pickle to h5ad + pickle_src = os.path.join( + self.tmp_dir.name, "convert_src.pickle" + ) + with open(pickle_src, "wb") as f: + pickle.dump(self.test_adata, f) + + h5ad_dest = convert_pickle_to_h5ad(pickle_src) + self.assertTrue(os.path.exists(h5ad_dest)) + self.assertTrue(h5ad_dest.endswith(".h5ad")) + + # Test with custom output path + custom_dest = os.path.join( + self.tmp_dir.name, "custom_output.h5ad" + ) + h5ad_custom = convert_pickle_to_h5ad(pickle_src, custom_dest) + self.assertEqual(h5ad_custom, custom_dest) + self.assertTrue(os.path.exists(custom_dest)) + + # Test 7: Load file with no extension (content detection) + no_ext_path = os.path.join(self.tmp_dir.name, "noextension") + with open(no_ext_path, "wb") as f: + pickle.dump(self.test_adata, f) + loaded_no_ext = load_input(no_ext_path) + self.assertEqual(loaded_no_ext.n_obs, 10) + + def test_text_to_value_conversions(self) -> None: + """Test all text_to_value conversion scenarios.""" + # Test 1: Convert to float + result = text_to_value("3.14", to_float=True) + self.assertEqual(result, 3.14) + self.assertIsInstance(result, float) + + # Test 2: Convert to int + result = text_to_value("42", to_int=True) + self.assertEqual(result, 42) + self.assertIsInstance(result, int) + + # Test 3: None text handling + result = text_to_value("None", value_to_convert_to=None) + self.assertIsNone(result) + + # Test 4: Empty string handling + result = text_to_value("", value_to_convert_to=-1) + self.assertEqual(result, -1) + + # Test 5: Case insensitive None + result = text_to_value("none", value_to_convert_to=0) + self.assertEqual(result, 0) + + # Test 6: Custom none text + result = text_to_value( + "NA", default_none_text="NA", value_to_convert_to=999 + ) + self.assertEqual(result, 999) + + # Test 7: No conversion + result = text_to_value("keep_as_string") + self.assertEqual(result, "keep_as_string") + self.assertIsInstance(result, str) + + # Test 8: Whitespace handling + result = text_to_value(" None ", value_to_convert_to=None) + self.assertIsNone(result) + + # Test 9: Non-string input + result = text_to_value(123, to_float=True) + self.assertEqual(result, 123.0) + self.assertIsInstance(result, float) + + def test_convert_to_floats(self) -> None: + """Test convert_to_floats function.""" + # Test 1: String list + result = convert_to_floats(["1.5", "2.0", "3.14"]) + self.assertEqual(result, [1.5, 2.0, 3.14]) + self.assertTrue(all(isinstance(x, float) for x in result)) + + # Test 2: Mixed numeric types + result = convert_to_floats([1, "2.5", 3.0]) + self.assertEqual(result, [1.0, 2.5, 3.0]) + + # Test 3: Invalid value + with self.assertRaises(ValueError) as context: + convert_to_floats(["1.0", "invalid", "3.0"]) + expected_msg = "Failed to convert the radius: 'invalid' to float" + self.assertIn(expected_msg, str(context.exception)) + + # Test 4: Empty list + result = convert_to_floats([]) + self.assertEqual(result, []) + + def test_load_input_missing_file_error_message(self) -> None: + """Test exact error message for missing input file.""" + missing_path = "/nonexistent/path/file.h5ad" + + with self.assertRaises(FileNotFoundError) as context: + load_input(missing_path) + + expected_msg = f"Input file not found: {missing_path}" + actual_msg = str(context.exception) + self.assertEqual(expected_msg, actual_msg) + + def test_load_input_unsupported_format_error_message(self) -> None: + """Test exact error message for unsupported file format.""" + # Create a text file with unsupported content + txt_path = os.path.join(self.tmp_dir.name, "test.txt") + with open(txt_path, "w") as f: + f.write("This is not a valid data file") + + with self.assertRaises(ValueError) as context: + load_input(txt_path) + + actual_msg = str(context.exception) + self.assertTrue(actual_msg.startswith("Unable to load file")) + self.assertIn("Supported formats: h5ad, pickle", actual_msg) + + def test_text_to_value_float_conversion_error_message(self) -> None: + """Test exact error message for invalid float conversion.""" + with self.assertRaises(ValueError) as context: + text_to_value( + "not_a_number", to_float=True, param_name="test_param" + ) + + expected_msg = ( + 'Error: can\'t convert test_param to float. ' + 'Received:"not_a_number"' + ) + actual_msg = str(context.exception) + self.assertEqual(expected_msg, actual_msg) + + def test_text_to_value_int_conversion_error_message(self) -> None: + """Test exact error message for invalid integer conversion.""" + with self.assertRaises(ValueError) as context: + text_to_value("3.14", to_int=True, param_name="count") + + expected_msg = ( + 'Error: can\'t convert count to integer. ' + 'Received:"3.14"' + ) + actual_msg = str(context.exception) + self.assertEqual(expected_msg, actual_msg) + + def test_convert_pickle_to_h5ad_missing_file_error_message(self) -> None: + """Test exact error message for missing pickle file.""" + missing_pickle = "/nonexistent/file.pickle" + + with self.assertRaises(FileNotFoundError) as context: + convert_pickle_to_h5ad(missing_pickle) + + expected_msg = f"Pickle file not found: {missing_pickle}" + actual_msg = str(context.exception) + self.assertEqual(expected_msg, actual_msg) + + def test_convert_pickle_to_h5ad_wrong_type_error_message(self) -> None: + """Test exact error message when pickle doesn't contain AnnData.""" + # Create pickle with wrong type + wrong_pickle = os.path.join(self.tmp_dir.name, "wrong_type.pickle") + with open(wrong_pickle, "wb") as f: + pickle.dump({"not": "anndata"}, f) + + with self.assertRaises(TypeError) as context: + convert_pickle_to_h5ad(wrong_pickle) + + expected_msg = "Loaded object is not AnnData, got " + actual_msg = str(context.exception) + self.assertEqual(expected_msg, actual_msg) + + def test_default_pickle_extension(self) -> None: + """Test that files without extension default to pickle.""" + outputs = { + "no_extension": self.test_adata + } + saved_files = save_outputs(outputs, self.tmp_dir.name) + + # Should have .pickle extension + filepath = saved_files["no_extension"] + self.assertTrue(filepath.endswith('.pickle')) + self.assertTrue(os.path.exists(filepath)) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From c88925913faf4c99c906e9b55a073759395faa78 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:41:11 -0400 Subject: [PATCH 011/102] feat(ripley_l_template): add ripley_l_template and unit tests --- src/spac/templates/ripley_l_template.py | 135 ++++++++++++ tests/templates/test_ripley_l_template.py | 245 ++++++++++++++++++++++ 2 files changed, 380 insertions(+) create mode 100644 src/spac/templates/ripley_l_template.py create mode 100644 tests/templates/test_ripley_l_template.py diff --git a/src/spac/templates/ripley_l_template.py b/src/spac/templates/ripley_l_template.py new file mode 100644 index 00000000..bd0bb344 --- /dev/null +++ b/src/spac/templates/ripley_l_template.py @@ -0,0 +1,135 @@ +""" +Platform-agnostic Ripley-L template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.ripley_l_template import run_from_json +>>> run_from_json("examples/ripley_l_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, List +# import pandas as pd + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.spatial_analysis import ripley_l +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, + convert_to_floats +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], Any]: + """ + Execute Ripley-L analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the adata object + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or AnnData + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed AnnData object + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + radii = params["Radii"] + annotation = params["Annotation"] + phenotypes = [params["Center_Phenotype"], params["Neighbor_Phenotype"]] + regions = params.get("Stratify_By", "None") + n_simulations = params.get("Number_of_Simulations", 100) + area = params.get("Area", "None") + seed = params.get("Seed", 42) + spatial_key = params.get("Spatial_Key", "spatial") + edge_correction = params.get("Edge_Correction", True) + + # Process parameters + regions = text_to_value( + regions, + default_none_text="None" + ) + + area = text_to_value( + area, + default_none_text="None", + value_to_convert_to=None, + to_float=True, + param_name='Area' + ) + + # Convert radii to floats + radii = convert_to_floats(radii) + + # Run the analysis + ripley_l( + adata, + annotation=annotation, + phenotypes=phenotypes, + distances=radii, + regions=regions, + n_simulations=n_simulations, + area=area, + seed=seed, + spatial_key=spatial_key, + edge_correction=edge_correction + ) + + print("Ripley-L analysis completed successfully.") + + # Handle results based on save_results flag + if save_results: + # Save outputs + outfile = params.get("Output_File", "transform_output.pickle") + # Default to pickle format if not specified + if not outfile.endswith(('.pickle', '.pkl', '.h5ad')): + outfile = outfile.replace('.h5ad', '.pickle') + + print(type(outfile)) + saved_files = save_outputs({outfile: adata}) + print(saved_files) + + print(f"Ripley-L completed → {str(saved_files[outfile])}") + print(adata) + return saved_files + else: + # Return the adata object directly for in-memory workflows + print("Returning AnnData object (not saving to file)") + return adata + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python ripley_l_template.py ") + sys.exit(1) + + saved_files = run_from_json(sys.argv[1]) + + if isinstance(saved_files, dict): + print("\nOutput files:") + for filename, filepath in saved_files.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned AnnData object") diff --git a/tests/templates/test_ripley_l_template.py b/tests/templates/test_ripley_l_template.py new file mode 100644 index 00000000..d24fce7f --- /dev/null +++ b/tests/templates/test_ripley_l_template.py @@ -0,0 +1,245 @@ +# tests/templates/test_ripley_l_template.py +"""Unit‑tests for the Ripley‑L template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +from unittest.mock import patch, MagicMock +import anndata as ad +import numpy as np +import pandas as pd + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.ripley_l_template import run_from_json + + +def mock_adata(n_cells: int = 40) -> ad.AnnData: + """Return a tiny synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame( + { + "renamed_phenotypes": np.where( + rng.random(n_cells) > 0.5, "B cells", "CD8 T cells" + ) + } + ) + x_mat = rng.normal(size=(n_cells, 3)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.obsm["spatial"] = rng.random((n_cells, 2)) * 300.0 + return adata + + +class TestRipleyLTemplate(unittest.TestCase): + """Light‑weight sanity checks for the Ripley‑L template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") + self.out_file = os.path.join(self.tmp_dir.name, "output.pickle") + + # Save as pickle + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + self.params = { + "Upstream_Analysis": self.in_file, + "Radii": [0, 50, 100], + "Annotation": "renamed_phenotypes", + "Center_Phenotype": "B cells", + "Neighbor_Phenotype": "CD8 T cells", + "Output_path": self.out_file, + "Stratify_By": "None", + "Number_of_Simulations": 100, + "Area": "None", + "Seed": 42, + "Spatial_Key": "spatial", + "Edge_Correction": True + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + @patch('spac.templates.ripley_l_template.ripley_l') + def test_run_from_dict_with_save(self, mock_ripley_l) -> None: + """Test run_from_json with dict parameters and file saving.""" + # Mock the ripley_l function + def mock_ripley_side_effect(adata, **kwargs): + # Simulate what ripley_l does - adds results to adata.uns + phenotypes = kwargs.get('phenotypes', ['B cells', 'CD8 T cells']) + key = f"ripley_l_{phenotypes[0]}_{phenotypes[1]}" + adata.uns[key] = { + "radius": kwargs.get('distances', [0, 50, 100]), + "ripley_l": [0.0, 1.2, 2.5], + "simulations": [[0.0, 0.8, 1.9], [0.0, 1.1, 2.3]], + } + return None + + mock_ripley_l.side_effect = mock_ripley_side_effect + + # Run with save_results=True (default) + saved_files = run_from_json(self.params) + + # Check that ripley_l was called with correct parameters + mock_ripley_l.assert_called_once() + call_args = mock_ripley_l.call_args + + # Verify the call arguments + self.assertEqual(call_args[1]['annotation'], "renamed_phenotypes") + self.assertEqual(call_args[1]['phenotypes'], ["B cells", "CD8 T cells"]) + self.assertEqual(call_args[1]['distances'], [0.0, 50.0, 100.0]) + self.assertEqual(call_args[1]['n_simulations'], 100) + self.assertEqual(call_args[1]['seed'], 42) + self.assertEqual(call_args[1]['spatial_key'], "spatial") + self.assertEqual(call_args[1]['edge_correction'], True) + + # Check that output file was created - check if any file was saved + self.assertTrue( + len(saved_files) > 0, + f"Expected files to be saved, but got {saved_files}" + ) + + # Check that at least one pickle file was created + pickle_files = [f for f in saved_files.values() + if f.endswith('.pickle')] + self.assertTrue( + len(pickle_files) > 0, + f"Expected at least one pickle file, got {saved_files}" + ) + + @patch('spac.templates.ripley_l_template.ripley_l') + def test_run_from_dict_without_save(self, mock_ripley_l) -> None: + """Test run_from_json with dict parameters and no file saving.""" + # Mock the ripley_l function + def mock_ripley_side_effect(adata, **kwargs): + phenotypes = kwargs.get('phenotypes', ['B cells', 'CD8 T cells']) + key = f"ripley_l_{phenotypes[0]}_{phenotypes[1]}" + adata.uns[key] = { + "radius": kwargs.get('distances', [0, 50, 100]), + "ripley_l": [0.0, 1.2, 2.5], + } + return None + + mock_ripley_l.side_effect = mock_ripley_side_effect + + # Run with save_results=False + adata_result = run_from_json(self.params, save_results=False) + + # Check that ripley_l was called + mock_ripley_l.assert_called_once() + + # Check that we got an AnnData object back + self.assertIsInstance(adata_result, ad.AnnData) + + # Check that results are in the object + ripley_key = "ripley_l_B cells_CD8 T cells" + self.assertIn(ripley_key, adata_result.uns) + + @patch('spac.templates.ripley_l_template.ripley_l') + def test_run_from_json_file(self, mock_ripley_l) -> None: + """Test run_from_json accepts a JSON file path.""" + # Mock the ripley_l function + def mock_ripley_side_effect(adata, **kwargs): + phenotypes = kwargs.get('phenotypes', ['B cells', 'CD8 T cells']) + key = f"ripley_l_{phenotypes[0]}_{phenotypes[1]}" + adata.uns[key] = { + "radius": kwargs.get('distances', [0, 50, 100]), + "ripley_l": [0.0, 1.2, 2.5], + "simulations": [[0.0, 0.8, 1.9], [0.0, 1.1, 2.3]], + } + return None + + mock_ripley_l.side_effect = mock_ripley_side_effect + + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as handle: + json.dump(self.params, handle) + + saved_files = run_from_json(json_path) + + # Verify ripley_l was called + mock_ripley_l.assert_called_once() + + # Check that save_outputs was called + mock_ripley_l.assert_called_once() + + # Check that files were saved + self.assertTrue(len(saved_files) > 0) + + @patch('spac.templates.ripley_l_template.ripley_l') + def test_radii_conversion(self, mock_ripley_l) -> None: + """Test that radii strings are converted to floats.""" + # Use string radii + params_str = self.params.copy() + params_str["Radii"] = ["0", "50", "100"] + + def mock_ripley_side_effect(adata, **kwargs): + phenotypes = kwargs.get('phenotypes', ['B cells', 'CD8 T cells']) + key = f"ripley_l_{phenotypes[0]}_{phenotypes[1]}" + adata.uns[key] = {"radius": [0, 50, 100], "ripley_l": [0, 1, 2]} + return None + + mock_ripley_l.side_effect = mock_ripley_side_effect + + run_from_json(params_str, save_results=False) + + # Check that radii were converted to floats + call_args = mock_ripley_l.call_args + self.assertEqual(call_args[1]['distances'], [0.0, 50.0, 100.0]) + + def test_invalid_radius_conversion(self) -> None: + """Test that invalid radius values raise appropriate errors.""" + params_bad = self.params.copy() + params_bad["Radii"] = ["0", "50", "invalid"] + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + expected_msg = "Failed to convert the radius: 'invalid' to float" + self.assertIn(expected_msg, str(context.exception)) + + def test_parameter_validation(self) -> None: + """Test that missing required parameters raise errors.""" + params_missing = self.params.copy() + del params_missing["Center_Phenotype"] + + with self.assertRaises(KeyError) as context: + run_from_json(params_missing) + + self.assertIn("Center_Phenotype", str(context.exception)) + + @patch('spac.templates.ripley_l_template.ripley_l') + def test_regions_parameter(self, mock_ripley_l) -> None: + """Test that regions parameter is processed correctly.""" + params_regions = self.params.copy() + params_regions["Stratify_By"] = "tumor_region" + + mock_ripley_l.side_effect = lambda adata, **kwargs: None + + run_from_json(params_regions, save_results=False) + + # Check that regions was passed correctly (not as "None") + call_args = mock_ripley_l.call_args + self.assertEqual(call_args[1]['regions'], "tumor_region") + + def test_pickle_output_format(self) -> None: + """Test that output defaults to pickle format.""" + params = self.params.copy() + params["Output_File"] = "results.dat" # No extension + + with patch('spac.templates.ripley_l_template.ripley_l'): + saved_files = run_from_json(params) + + # Should save as pickle by default + pickle_files = [f for f in saved_files.values() + if '.pickle' in str(f)] + self.assertTrue(len(pickle_files) > 0) + + +if __name__ == "__main__": + unittest.main() From 48608e26ca16c1e89a573286553b370f2a3f508b Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:56:12 -0400 Subject: [PATCH 012/102] feat(visualize_ripley_template): add visualize_ripley_template and unit tests --- .../templates/visualize_ripley_template.py | 117 ++++++++++ .../test_visualize_ripley_template.py | 217 ++++++++++++++++++ 2 files changed, 334 insertions(+) create mode 100644 src/spac/templates/visualize_ripley_template.py create mode 100644 tests/templates/test_visualize_ripley_template.py diff --git a/src/spac/templates/visualize_ripley_template.py b/src/spac/templates/visualize_ripley_template.py new file mode 100644 index 00000000..d8311588 --- /dev/null +++ b/src/spac/templates/visualize_ripley_template.py @@ -0,0 +1,117 @@ +""" +Platform-agnostic Visualize Ripley L template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.visualize_ripley_template import run_from_json +>>> run_from_json("examples/visualize_ripley_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, List, Optional, Tuple +import pandas as pd +import matplotlib.pyplot as plt + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.visualization import plot_ripley_l +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: + """ + Execute Visualize Ripley L analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the figure and + dataframe directly for in-memory workflows. Default is True. + + Returns + ------- + dict or tuple + If save_results=True: Dictionary of saved file paths + If save_results=False: Tuple of (figure, dataframe) + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + center_phenotype = params["Center_Phenotype"] + neighbor_phenotype = params["Neighbor_Phenotype"] + plot_specific_regions = params.get("Plot_Specific_Regions", False) + regions_labels = params.get("Regions_Labels", []) + plot_simulations = params.get("Plot_Simulations", True) + + print(f"running with center_phenotype: {center_phenotype}, neighbor_phenotype: {neighbor_phenotype}") + + # Process regions parameter exactly as in NIDAP template + if plot_specific_regions: + if len(regions_labels) == 0: + raise ValueError( + 'Please identify at least one region in the ' + '"Regions Label(s) parameter' + ) + else: + regions_labels = None + + # Run the visualization exactly as in NIDAP template + fig, plots_df = plot_ripley_l( + adata, + phenotypes=(center_phenotype, neighbor_phenotype), + regions=regions_labels, + sims=plot_simulations, + return_df=True + ) + + plt.show() + + # Print the dataframe to console + print(plots_df.to_string()) + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "plots.csv") + saved_files = save_outputs({output_file: plots_df}) + + print(f"Visualize Ripley L completed → {saved_files[output_file]}") + return saved_files + else: + # Return the figure and dataframe directly for in-memory workflows + print("Returning figure and dataframe (not saving to file)") + return fig, plots_df + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python visualize_ripley_template.py ") + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned figure and dataframe") \ No newline at end of file diff --git a/tests/templates/test_visualize_ripley_template.py b/tests/templates/test_visualize_ripley_template.py new file mode 100644 index 00000000..6129cfc4 --- /dev/null +++ b/tests/templates/test_visualize_ripley_template.py @@ -0,0 +1,217 @@ +# tests/templates/test_visualize_ripley_template.py +"""Unit tests for the Visualize Ripley L template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings + +import matplotlib +matplotlib.use("Agg") # Headless backend for CI + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path +from unittest.mock import patch, MagicMock + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.visualize_ripley_template import run_from_json + + +def mock_adata_with_ripley(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData with Ripley L results for tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "phenotype": ["B cells", "CD8 T cells"] * (n_cells // 2) + }) + x_mat = rng.normal(size=(n_cells, 2)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.obsm["spatial"] = rng.random((n_cells, 2)) * 50.0 + + # Add mock Ripley L results in the expected format + # When using pickle, the structure is preserved as-is + adata.uns["ripley_l_B cells_CD8 T cells"] = { + "radius": [0, 50, 100], + "ripley_l": [0, 1.2, 2.5], + "simulations": np.array([ + [0, 0.8, 1.9], [0, 1.1, 2.3], [0, 1.3, 2.7] + ]) + } + return adata + + +class TestVisualizeRipleyTemplate(unittest.TestCase): + """Unit tests for the Visualize Ripley L template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "ripley_output.pickle" + ) + self.out_file = "plots.csv" + + # Save minimal mock data with Ripley results as pickle + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata_with_ripley(), f) + + # Minimal parameters + self.params = { + "Upstream_Analysis": self.in_file, + "Center_Phenotype": "B cells", + "Neighbor_Phenotype": "CD8 T cells", + "Plot_Specific_Regions": False, + "Regions_Labels": [], + "Plot_Simulations": True, + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + @patch('spac.templates.visualize_ripley_template.plot_ripley_l') + def test_run_with_save(self, mock_plot_ripley) -> None: + """Test visualization with file saving.""" + # Mock the plot_ripley_l function to return a figure and dataframe + mock_fig = MagicMock() + mock_df = pd.DataFrame({ + 'radius': [0, 50, 100], + 'ripley_l': [0, 1.2, 2.5], + 'lower_ci': [0, 0.8, 1.9], + 'upper_ci': [0, 1.6, 3.1] + }) + mock_plot_ripley.return_value = (mock_fig, mock_df) + + # Suppress warnings for cleaner test output + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test with save_results=True (default) + saved_files = run_from_json(self.params) + self.assertIn(self.out_file, saved_files) + + # Verify plot_ripley_l was called with correct parameters + mock_plot_ripley.assert_called_once() + call_args = mock_plot_ripley.call_args + # Check that adata was passed as first argument + self.assertEqual(call_args[0][0].n_obs, 10) + # Check keyword arguments + self.assertEqual( + call_args[1]['phenotypes'], ("B cells", "CD8 T cells") + ) + self.assertEqual(call_args[1]['regions'], None) + self.assertEqual(call_args[1]['sims'], True) + self.assertEqual(call_args[1]['return_df'], True) + + @patch('spac.templates.visualize_ripley_template.plot_ripley_l') + def test_run_without_save(self, mock_plot_ripley) -> None: + """Test visualization without file saving.""" + # Mock the plot_ripley_l function + mock_fig = MagicMock() + mock_df = pd.DataFrame({ + 'radius': [0, 50, 100], + 'ripley_l': [0, 1.2, 2.5] + }) + mock_plot_ripley.return_value = (mock_fig, mock_df) + + # Test with save_results=False + fig, df = run_from_json(self.params, save_results=False) + + # Verify we got the figure and dataframe back + self.assertEqual(fig, mock_fig) + self.assertIsInstance(df, pd.DataFrame) + self.assertEqual(len(df), 3) + self.assertIn('radius', df.columns) + self.assertIn('ripley_l', df.columns) + + @patch('spac.templates.visualize_ripley_template.plot_ripley_l') + def test_with_specific_regions(self, mock_plot_ripley) -> None: + """Test with specific regions enabled.""" + params_regions = self.params.copy() + params_regions["Plot_Specific_Regions"] = True + params_regions["Regions_Labels"] = ["Region1", "Region2"] + + mock_fig = MagicMock() + mock_df = pd.DataFrame({'radius': [0], 'ripley_l': [0]}) + mock_plot_ripley.return_value = (mock_fig, mock_df) + + run_from_json(params_regions, save_results=False) + + # Verify regions parameter was passed correctly + call_args = mock_plot_ripley.call_args + self.assertEqual( + call_args[1]['regions'], ["Region1", "Region2"] + ) + + def test_regions_validation_error_message(self) -> None: + """ + Test exact error message for empty regions + when Plot_Specific_Regions is True. + """ + params_bad = self.params.copy() + params_bad["Plot_Specific_Regions"] = True + params_bad["Regions_Labels"] = [] + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + expected_msg = ( + 'Please identify at least one region in the ' + '"Regions Label(s) parameter' + ) + actual_msg = str(context.exception) + self.assertEqual(expected_msg, actual_msg) + + @patch('spac.templates.visualize_ripley_template.plot_ripley_l') + @patch('builtins.print') + def test_console_output(self, mock_print, mock_plot_ripley) -> None: + """Test that dataframe is printed to console.""" + # Mock the plot_ripley_l function + mock_fig = MagicMock() + mock_df = pd.DataFrame({ + 'radius': [0, 50, 100], + 'ripley_l': [0, 1.2, 2.5] + }) + mock_plot_ripley.return_value = (mock_fig, mock_df) + + run_from_json(self.params, save_results=False) + + # Verify dataframe was printed + print_calls = mock_print.call_args_list + # Check that to_string() output was printed + df_printed = False + for call in print_calls: + if (len(call[0]) > 0 and + str(call[0][0]) == mock_df.to_string()): + df_printed = True + break + self.assertTrue( + df_printed, "DataFrame was not printed to console" + ) + + @patch('spac.templates.visualize_ripley_template.plot_ripley_l') + def test_json_file_input(self, mock_plot_ripley) -> None: + """Test with JSON file input.""" + mock_fig = MagicMock() + mock_df = pd.DataFrame({'radius': [0], 'ripley_l': [0]}) + mock_plot_ripley.return_value = (mock_fig, mock_df) + + json_path = os.path.join(self.tmp_dir.name, "viz_params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result = run_from_json(json_path, save_results=False) + + # Should return tuple when save_results=False + self.assertIsInstance(result, tuple) + self.assertEqual(len(result), 2) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From ff9238c126ab5e524608b24c28a47bebc3ed487d Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Thu, 24 Jul 2025 17:00:59 -0400 Subject: [PATCH 013/102] fix(visualize_ripley): add missing __init__.py for templates module --- src/spac/templates/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/spac/templates/__init__.py diff --git a/src/spac/templates/__init__.py b/src/spac/templates/__init__.py new file mode 100644 index 00000000..89c61771 --- /dev/null +++ b/src/spac/templates/__init__.py @@ -0,0 +1,13 @@ +""" +Canonical SPAC template sub‑package. + +Each template is a self‑contained module that + • reads parameters from JSON/dict + • runs a SPAC analysis function + • returns / saves results + +Available templates +------------------- +- ripley_l_template.run_from_json +""" + From 0e2747e8d05547c2e2dca4ad9e2b8ec730e24260 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Thu, 24 Jul 2025 17:51:32 -0400 Subject: [PATCH 014/102] fix(visualize_ripley): make plt.show() conditional based on show_plot parameter --- .../templates/visualize_ripley_template.py | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/spac/templates/visualize_ripley_template.py b/src/spac/templates/visualize_ripley_template.py index d8311588..01036bb6 100644 --- a/src/spac/templates/visualize_ripley_template.py +++ b/src/spac/templates/visualize_ripley_template.py @@ -28,12 +28,13 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_results: bool = True, + show_plot: bool = True ) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: """ Execute Visualize Ripley L analysis with parameters from JSON. Replicates the NIDAP template functionality exactly. - + Parameters ---------- json_path : str, Path, or dict @@ -41,7 +42,9 @@ def run_from_json( save_results : bool, optional Whether to save results to file. If False, returns the figure and dataframe directly for in-memory workflows. Default is True. - + show_plot : bool, optional + Whether to display the plot. Default is True. + Returns ------- dict or tuple @@ -50,10 +53,10 @@ def run_from_json( """ # Parse parameters from JSON params = parse_params(json_path) - + # Load the upstream analysis data adata = load_input(params["Upstream_Analysis"]) - + # Extract parameters center_phenotype = params["Center_Phenotype"] neighbor_phenotype = params["Neighbor_Phenotype"] @@ -82,17 +85,18 @@ def run_from_json( return_df=True ) - plt.show() - + if show_plot: + plt.show() + # Print the dataframe to console print(plots_df.to_string()) - + # Handle results based on save_results flag if save_results: # Save outputs output_file = params.get("Output_File", "plots.csv") saved_files = save_outputs({output_file: plots_df}) - + print(f"Visualize Ripley L completed → {saved_files[output_file]}") return saved_files else: @@ -106,12 +110,12 @@ def run_from_json( if len(sys.argv) != 2: print("Usage: python visualize_ripley_template.py ") sys.exit(1) - + result = run_from_json(sys.argv[1]) - + if isinstance(result, dict): print("\nOutput files:") for filename, filepath in result.items(): print(f" {filename}: {filepath}") else: - print("\nReturned figure and dataframe") \ No newline at end of file + print("\nReturned figure and dataframe") From 7b7e6cbbe3bf9ea591de2075cb40861c2136a879 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Thu, 24 Jul 2025 18:20:36 -0400 Subject: [PATCH 015/102] fix: add missing __init__.py in tests/templates --- tests/templates/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/templates/__init__.py diff --git a/tests/templates/__init__.py b/tests/templates/__init__.py new file mode 100644 index 00000000..e69de29b From e9f088335c76c093aa75edc81b3bab33b53a30c8 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Thu, 24 Jul 2025 18:52:40 -0400 Subject: [PATCH 016/102] fix(template_utils): address review comments --- src/spac/templates/template_utils.py | 62 +++++++++++++------------- tests/templates/test_template_utils.py | 34 +++++++------- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/src/spac/templates/template_utils.py b/src/spac/templates/template_utils.py index aeccb3ae..9a866c00 100644 --- a/src/spac/templates/template_utils.py +++ b/src/spac/templates/template_utils.py @@ -1,25 +1,26 @@ from pathlib import Path import pickle -from typing import Any, Dict, Union, Optional, List +from typing import Any, Dict, Union, Optional, List import json import anndata as ad - +import logging +logger = logging.getLogger(__name__) def load_input(file_path: Union[str, Path]): """ Load input data from either h5ad or pickle file. - + Parameters ---------- file_path : str or Path Path to input file (h5ad or pickle) - + Returns ------- Loaded data object (typically AnnData) """ path = Path(file_path) - + if not path.exists(): raise FileNotFoundError(f"Input file not found: {file_path}") @@ -68,7 +69,7 @@ def save_outputs( """ Save multiple outputs to files and return a dict {filename: absolute_path}. (Always a dict, even if just one file.) - + Parameters ---------- outputs : dict @@ -77,12 +78,12 @@ def save_outputs( - value: object to save output_dir : str or Path Directory to save files - + Returns ------- dict Dictionary of saved file paths - + Example ------- >>> outputs = { @@ -94,12 +95,12 @@ def save_outputs( """ output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) - + saved_files = {} - + for filename, obj in outputs.items(): filepath = output_dir / filename - + # Save based on file extension if filename.endswith('.csv'): obj.to_csv(filepath, index=False) @@ -109,10 +110,10 @@ def save_outputs( raise TypeError( f"Object for '{str(filename)}' must be AnnData, got {type(obj)}" ) - print(f"Saving AnnData to {str(filepath)}") + logger.info(f"Saving AnnData to {str(filepath)}") print(obj) obj.write_h5ad(str(filepath)) - print(f"Saved AnnData to {str(filepath)}") + logger.info(f"Saved AnnData to {str(filepath)}") elif filename.endswith(('.pickle', '.pkl', '.p')): with open(filepath, 'wb') as f: pickle.dump(obj, f) @@ -125,12 +126,11 @@ def save_outputs( with open(filepath, 'wb') as f: pickle.dump(obj, f) - # filepath = filepath.resolve() print(type(filepath)) print(type(filename)) saved_files[str(filename)] = str(filepath) print(f"Saved: {filepath}") - + return saved_files @@ -139,12 +139,12 @@ def parse_params( ) -> Dict[str, Any]: """ Parse parameters from JSON file, string, or dict. - + Parameters ---------- json_input : str, Path, or dict JSON file path, JSON string, or dictionary - + Returns ------- dict @@ -152,10 +152,10 @@ def parse_params( """ if isinstance(json_input, dict): return json_input - + if isinstance(json_input, (str, Path)): path = Path(json_input) - + # Check if it's a file path if path.exists() or str(json_input).endswith('.json'): with open(path, 'r') as file: @@ -163,7 +163,7 @@ def parse_params( else: # It's a JSON string return json.loads(str(json_input)) - + raise TypeError( "json_input must be dict, JSON string, or path to JSON file" ) @@ -250,7 +250,7 @@ def text_to_value( var.lower().strip() == default_none_text.lower().strip() or var.strip() == '' ) - + if none_condition: var = value_to_convert_to @@ -301,7 +301,7 @@ def convert_to_floats(text_list: List[Any]) -> List[float]: try: float_list.append(float(value)) except ValueError: - msg = f"Failed to convert the radius: '{value}' to float." + msg = f"Failed to convert value : '{value}' to float." raise ValueError(msg) return float_list @@ -312,7 +312,7 @@ def convert_pickle_to_h5ad( ) -> str: """ Convert a pickle file containing AnnData to h5ad format. - + Parameters ---------- pickle_path : str or Path @@ -320,21 +320,21 @@ def convert_pickle_to_h5ad( h5ad_path : str or Path, optional Path for output h5ad file. If None, uses same name with .h5ad extension - + Returns ------- str Path to saved h5ad file """ pickle_path = Path(pickle_path) - + if not pickle_path.exists(): raise FileNotFoundError(f"Pickle file not found: {pickle_path}") - + # Load from pickle with pickle_path.open('rb') as fh: adata = pickle.load(fh) - + # Check if it's AnnData try: import anndata as ad @@ -346,14 +346,14 @@ def convert_pickle_to_h5ad( raise ImportError( "anndata package required for conversion to h5ad" ) - + # Determine output path if h5ad_path is None: h5ad_path = pickle_path.with_suffix('.h5ad') else: h5ad_path = Path(h5ad_path) - + # Save as h5ad adata.write_h5ad(h5ad_path) - - return str(h5ad_path) \ No newline at end of file + + return str(h5ad_path) diff --git a/tests/templates/test_template_utils.py b/tests/templates/test_template_utils.py index d6b424b0..aac05468 100644 --- a/tests/templates/test_template_utils.py +++ b/tests/templates/test_template_utils.py @@ -1,4 +1,4 @@ -# tests/utils/test_template_utils.py +# tests//templates/test_template_utils.py """Unit tests for template utilities.""" import json @@ -100,7 +100,7 @@ def test_complete_io_workflow(self) -> None: "other_data": {"key": "value"} # Defaults to pickle } saved_files = save_outputs(outputs, self.tmp_dir.name) - + # Verify all files were saved self.assertEqual(len(saved_files), 5) for filename, filepath in saved_files.items(): @@ -119,11 +119,11 @@ def test_complete_io_workflow(self) -> None: ) with open(pickle_src, "wb") as f: pickle.dump(self.test_adata, f) - + h5ad_dest = convert_pickle_to_h5ad(pickle_src) self.assertTrue(os.path.exists(h5ad_dest)) self.assertTrue(h5ad_dest.endswith(".h5ad")) - + # Test with custom output path custom_dest = os.path.join( self.tmp_dir.name, "custom_output.h5ad" @@ -197,7 +197,7 @@ def test_convert_to_floats(self) -> None: # Test 3: Invalid value with self.assertRaises(ValueError) as context: convert_to_floats(["1.0", "invalid", "3.0"]) - expected_msg = "Failed to convert the radius: 'invalid' to float" + expected_msg = "Failed to convert value : 'invalid' to float" self.assertIn(expected_msg, str(context.exception)) # Test 4: Empty list @@ -207,10 +207,10 @@ def test_convert_to_floats(self) -> None: def test_load_input_missing_file_error_message(self) -> None: """Test exact error message for missing input file.""" missing_path = "/nonexistent/path/file.h5ad" - + with self.assertRaises(FileNotFoundError) as context: load_input(missing_path) - + expected_msg = f"Input file not found: {missing_path}" actual_msg = str(context.exception) self.assertEqual(expected_msg, actual_msg) @@ -221,10 +221,10 @@ def test_load_input_unsupported_format_error_message(self) -> None: txt_path = os.path.join(self.tmp_dir.name, "test.txt") with open(txt_path, "w") as f: f.write("This is not a valid data file") - + with self.assertRaises(ValueError) as context: load_input(txt_path) - + actual_msg = str(context.exception) self.assertTrue(actual_msg.startswith("Unable to load file")) self.assertIn("Supported formats: h5ad, pickle", actual_msg) @@ -235,7 +235,7 @@ def test_text_to_value_float_conversion_error_message(self) -> None: text_to_value( "not_a_number", to_float=True, param_name="test_param" ) - + expected_msg = ( 'Error: can\'t convert test_param to float. ' 'Received:"not_a_number"' @@ -247,7 +247,7 @@ def test_text_to_value_int_conversion_error_message(self) -> None: """Test exact error message for invalid integer conversion.""" with self.assertRaises(ValueError) as context: text_to_value("3.14", to_int=True, param_name="count") - + expected_msg = ( 'Error: can\'t convert count to integer. ' 'Received:"3.14"' @@ -258,10 +258,10 @@ def test_text_to_value_int_conversion_error_message(self) -> None: def test_convert_pickle_to_h5ad_missing_file_error_message(self) -> None: """Test exact error message for missing pickle file.""" missing_pickle = "/nonexistent/file.pickle" - + with self.assertRaises(FileNotFoundError) as context: convert_pickle_to_h5ad(missing_pickle) - + expected_msg = f"Pickle file not found: {missing_pickle}" actual_msg = str(context.exception) self.assertEqual(expected_msg, actual_msg) @@ -272,10 +272,10 @@ def test_convert_pickle_to_h5ad_wrong_type_error_message(self) -> None: wrong_pickle = os.path.join(self.tmp_dir.name, "wrong_type.pickle") with open(wrong_pickle, "wb") as f: pickle.dump({"not": "anndata"}, f) - + with self.assertRaises(TypeError) as context: convert_pickle_to_h5ad(wrong_pickle) - + expected_msg = "Loaded object is not AnnData, got " actual_msg = str(context.exception) self.assertEqual(expected_msg, actual_msg) @@ -286,7 +286,7 @@ def test_default_pickle_extension(self) -> None: "no_extension": self.test_adata } saved_files = save_outputs(outputs, self.tmp_dir.name) - + # Should have .pickle extension filepath = saved_files["no_extension"] self.assertTrue(filepath.endswith('.pickle')) @@ -294,4 +294,4 @@ def test_default_pickle_extension(self) -> None: if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From c94b219f30b551caeffa5074dda6173cf3a9ab8f Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Thu, 24 Jul 2025 19:16:35 -0400 Subject: [PATCH 017/102] fix(template_utils): address review comments again --- src/spac/templates/template_utils.py | 4 ++-- tests/templates/test_template_utils.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spac/templates/template_utils.py b/src/spac/templates/template_utils.py index 9a866c00..81f3c208 100644 --- a/src/spac/templates/template_utils.py +++ b/src/spac/templates/template_utils.py @@ -111,7 +111,7 @@ def save_outputs( f"Object for '{str(filename)}' must be AnnData, got {type(obj)}" ) logger.info(f"Saving AnnData to {str(filepath)}") - print(obj) + logger.debug(f"AnnData object: {obj}") obj.write_h5ad(str(filepath)) logger.info(f"Saved AnnData to {str(filepath)}") elif filename.endswith(('.pickle', '.pkl', '.p')): @@ -301,7 +301,7 @@ def convert_to_floats(text_list: List[Any]) -> List[float]: try: float_list.append(float(value)) except ValueError: - msg = f"Failed to convert value : '{value}' to float." + msg = f"Failed to convert value: '{value}' to float." raise ValueError(msg) return float_list diff --git a/tests/templates/test_template_utils.py b/tests/templates/test_template_utils.py index aac05468..886b33d8 100644 --- a/tests/templates/test_template_utils.py +++ b/tests/templates/test_template_utils.py @@ -1,4 +1,4 @@ -# tests//templates/test_template_utils.py +# tests/templates/test_template_utils.py """Unit tests for template utilities.""" import json @@ -197,7 +197,7 @@ def test_convert_to_floats(self) -> None: # Test 3: Invalid value with self.assertRaises(ValueError) as context: convert_to_floats(["1.0", "invalid", "3.0"]) - expected_msg = "Failed to convert value : 'invalid' to float" + expected_msg = "Failed to convert value: 'invalid' to float" self.assertIn(expected_msg, str(context.exception)) # Test 4: Empty list From 9914716db356d8ca217963a0d95c85bb37e2ff19 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Thu, 24 Jul 2025 19:57:07 -0400 Subject: [PATCH 018/102] fix(ripley_template): address review comments - replace debug prints with logging --- src/spac/templates/ripley_l_template.py | 23 ++++---- tests/templates/test_ripley_l_template.py | 66 +++++++++++------------ 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/src/spac/templates/ripley_l_template.py b/src/spac/templates/ripley_l_template.py index bd0bb344..be12bae6 100644 --- a/src/spac/templates/ripley_l_template.py +++ b/src/spac/templates/ripley_l_template.py @@ -11,7 +11,8 @@ import sys from pathlib import Path from typing import Any, Dict, Union, List -# import pandas as pd +import logging +logger = logging.getLogger(__name__) # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) @@ -24,7 +25,7 @@ text_to_value, convert_to_floats ) - + def run_from_json( json_path: Union[str, Path, Dict[str, Any]], @@ -106,30 +107,30 @@ def run_from_json( if not outfile.endswith(('.pickle', '.pkl', '.h5ad')): outfile = outfile.replace('.h5ad', '.pickle') - print(type(outfile)) + logging.debug(f"Output file type: {type(outfile)}") saved_files = save_outputs({outfile: adata}) - print(saved_files) + logging.debug(f"Saved files: {saved_files}") - print(f"Ripley-L completed → {str(saved_files[outfile])}") - print(adata) + logging.info(f"Ripley-L completed → {str(saved_files[outfile])}") + logging.debug(f"AnnData object: {adata}") return saved_files else: # Return the adata object directly for in-memory workflows - print("Returning AnnData object (not saving to file)") + logging.info("Returning AnnData object (not saving to file)") return adata # CLI interface if __name__ == "__main__": if len(sys.argv) != 2: - print("Usage: python ripley_l_template.py ") + logging.error("Usage: python ripley_l_template.py ") sys.exit(1) saved_files = run_from_json(sys.argv[1]) if isinstance(saved_files, dict): - print("\nOutput files:") + logging.info("\nOutput files:") for filename, filepath in saved_files.items(): - print(f" {filename}: {filepath}") + logging.info(f" {filename}: {filepath}") else: - print("\nReturned AnnData object") + logging.info("\nReturned AnnData object") diff --git a/tests/templates/test_ripley_l_template.py b/tests/templates/test_ripley_l_template.py index d24fce7f..f48e100a 100644 --- a/tests/templates/test_ripley_l_template.py +++ b/tests/templates/test_ripley_l_template.py @@ -1,5 +1,5 @@ # tests/templates/test_ripley_l_template.py -"""Unit‑tests for the Ripley‑L template.""" +"""Unit-tests for the Ripley-L template.""" import json import os @@ -36,7 +36,7 @@ def mock_adata(n_cells: int = 40) -> ad.AnnData: class TestRipleyLTemplate(unittest.TestCase): - """Light‑weight sanity checks for the Ripley‑L template.""" + """Light-weight sanity checks for the Ripley-L template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() @@ -68,7 +68,7 @@ def tearDown(self) -> None: @patch('spac.templates.ripley_l_template.ripley_l') def test_run_from_dict_with_save(self, mock_ripley_l) -> None: """Test run_from_json with dict parameters and file saving.""" - # Mock the ripley_l function + # Mock the ripley_l function def mock_ripley_side_effect(adata, **kwargs): # Simulate what ripley_l does - adds results to adata.uns phenotypes = kwargs.get('phenotypes', ['B cells', 'CD8 T cells']) @@ -79,16 +79,16 @@ def mock_ripley_side_effect(adata, **kwargs): "simulations": [[0.0, 0.8, 1.9], [0.0, 1.1, 2.3]], } return None - + mock_ripley_l.side_effect = mock_ripley_side_effect # Run with save_results=True (default) saved_files = run_from_json(self.params) - + # Check that ripley_l was called with correct parameters mock_ripley_l.assert_called_once() call_args = mock_ripley_l.call_args - + # Verify the call arguments self.assertEqual(call_args[1]['annotation'], "renamed_phenotypes") self.assertEqual(call_args[1]['phenotypes'], ["B cells", "CD8 T cells"]) @@ -97,15 +97,15 @@ def mock_ripley_side_effect(adata, **kwargs): self.assertEqual(call_args[1]['seed'], 42) self.assertEqual(call_args[1]['spatial_key'], "spatial") self.assertEqual(call_args[1]['edge_correction'], True) - + # Check that output file was created - check if any file was saved self.assertTrue( len(saved_files) > 0, f"Expected files to be saved, but got {saved_files}" ) - + # Check that at least one pickle file was created - pickle_files = [f for f in saved_files.values() + pickle_files = [f for f in saved_files.values() if f.endswith('.pickle')] self.assertTrue( len(pickle_files) > 0, @@ -124,18 +124,18 @@ def mock_ripley_side_effect(adata, **kwargs): "ripley_l": [0.0, 1.2, 2.5], } return None - + mock_ripley_l.side_effect = mock_ripley_side_effect - + # Run with save_results=False adata_result = run_from_json(self.params, save_results=False) - + # Check that ripley_l was called mock_ripley_l.assert_called_once() - + # Check that we got an AnnData object back self.assertIsInstance(adata_result, ad.AnnData) - + # Check that results are in the object ripley_key = "ripley_l_B cells_CD8 T cells" self.assertIn(ripley_key, adata_result.uns) @@ -153,18 +153,18 @@ def mock_ripley_side_effect(adata, **kwargs): "simulations": [[0.0, 0.8, 1.9], [0.0, 1.1, 2.3]], } return None - + mock_ripley_l.side_effect = mock_ripley_side_effect - + json_path = os.path.join(self.tmp_dir.name, "params.json") with open(json_path, "w") as handle: json.dump(self.params, handle) - + saved_files = run_from_json(json_path) - + # Verify ripley_l was called mock_ripley_l.assert_called_once() - + # Check that save_outputs was called mock_ripley_l.assert_called_once() @@ -177,17 +177,17 @@ def test_radii_conversion(self, mock_ripley_l) -> None: # Use string radii params_str = self.params.copy() params_str["Radii"] = ["0", "50", "100"] - + def mock_ripley_side_effect(adata, **kwargs): phenotypes = kwargs.get('phenotypes', ['B cells', 'CD8 T cells']) key = f"ripley_l_{phenotypes[0]}_{phenotypes[1]}" adata.uns[key] = {"radius": [0, 50, 100], "ripley_l": [0, 1, 2]} return None - + mock_ripley_l.side_effect = mock_ripley_side_effect run_from_json(params_str, save_results=False) - + # Check that radii were converted to floats call_args = mock_ripley_l.call_args self.assertEqual(call_args[1]['distances'], [0.0, 50.0, 100.0]) @@ -196,21 +196,21 @@ def test_invalid_radius_conversion(self) -> None: """Test that invalid radius values raise appropriate errors.""" params_bad = self.params.copy() params_bad["Radii"] = ["0", "50", "invalid"] - + with self.assertRaises(ValueError) as context: run_from_json(params_bad) - - expected_msg = "Failed to convert the radius: 'invalid' to float" + + expected_msg = "Failed to convert value : 'invalid' to float" self.assertIn(expected_msg, str(context.exception)) def test_parameter_validation(self) -> None: """Test that missing required parameters raise errors.""" params_missing = self.params.copy() del params_missing["Center_Phenotype"] - + with self.assertRaises(KeyError) as context: run_from_json(params_missing) - + self.assertIn("Center_Phenotype", str(context.exception)) @patch('spac.templates.ripley_l_template.ripley_l') @@ -218,11 +218,11 @@ def test_regions_parameter(self, mock_ripley_l) -> None: """Test that regions parameter is processed correctly.""" params_regions = self.params.copy() params_regions["Stratify_By"] = "tumor_region" - + mock_ripley_l.side_effect = lambda adata, **kwargs: None - + run_from_json(params_regions, save_results=False) - + # Check that regions was passed correctly (not as "None") call_args = mock_ripley_l.call_args self.assertEqual(call_args[1]['regions'], "tumor_region") @@ -231,12 +231,12 @@ def test_pickle_output_format(self) -> None: """Test that output defaults to pickle format.""" params = self.params.copy() params["Output_File"] = "results.dat" # No extension - + with patch('spac.templates.ripley_l_template.ripley_l'): saved_files = run_from_json(params) - + # Should save as pickle by default - pickle_files = [f for f in saved_files.values() + pickle_files = [f for f in saved_files.values() if '.pickle' in str(f)] self.assertTrue(len(pickle_files) > 0) From 415df89b0d3d368fa126b6167d2fb68028a8b512 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:01:43 -0400 Subject: [PATCH 019/102] fix(ripley_template): address review comments - merge dev into the branch and fix unit test --- src/spac/templates/ripley_l_template.py | 9 ++++----- tests/templates/test_ripley_l_template.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/spac/templates/ripley_l_template.py b/src/spac/templates/ripley_l_template.py index be12bae6..a787b3ac 100644 --- a/src/spac/templates/ripley_l_template.py +++ b/src/spac/templates/ripley_l_template.py @@ -12,7 +12,6 @@ from pathlib import Path from typing import Any, Dict, Union, List import logging -logger = logging.getLogger(__name__) # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) @@ -123,14 +122,14 @@ def run_from_json( # CLI interface if __name__ == "__main__": if len(sys.argv) != 2: - logging.error("Usage: python ripley_l_template.py ") + print("Usage: python ripley_l_template.py ", file=sys.stderr) sys.exit(1) saved_files = run_from_json(sys.argv[1]) if isinstance(saved_files, dict): - logging.info("\nOutput files:") + print("\nOutput files:") for filename, filepath in saved_files.items(): - logging.info(f" {filename}: {filepath}") + print(f" {filename}: {filepath}") else: - logging.info("\nReturned AnnData object") + print("\nReturned AnnData object") diff --git a/tests/templates/test_ripley_l_template.py b/tests/templates/test_ripley_l_template.py index f48e100a..bde5a9de 100644 --- a/tests/templates/test_ripley_l_template.py +++ b/tests/templates/test_ripley_l_template.py @@ -200,7 +200,7 @@ def test_invalid_radius_conversion(self) -> None: with self.assertRaises(ValueError) as context: run_from_json(params_bad) - expected_msg = "Failed to convert value : 'invalid' to float" + expected_msg = "Failed to convert value: 'invalid' to float" self.assertIn(expected_msg, str(context.exception)) def test_parameter_validation(self) -> None: From 5456658e8d7b0ab0475ead65bbe982ccefbacf47 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sun, 27 Jul 2025 18:36:57 -0400 Subject: [PATCH 020/102] feat(load_csv): Add load_csv template function with configuration support - Add load_csv_files() to template_utils.py for loading and combining CSV files - Add spell_out_special_characters() to handle biological marker names - Add load_csv_files_with_config.py template wrapper for NIDAP compatibility - Add comprehensive unit tests for both functions - Support column name cleaning, metadata mapping, and string column enforcement --- .../templates/load_csv_files_with_config.py | 98 +++++++ src/spac/templates/template_utils.py | 256 +++++++++++++++- .../test_load_csv_files_with_config.py | 275 ++++++++++++++++++ tests/templates/test_template_utils.py | 232 ++++++++++++++- 4 files changed, 857 insertions(+), 4 deletions(-) create mode 100644 src/spac/templates/load_csv_files_with_config.py create mode 100644 tests/templates/test_load_csv_files_with_config.py diff --git a/src/spac/templates/load_csv_files_with_config.py b/src/spac/templates/load_csv_files_with_config.py new file mode 100644 index 00000000..fc0d192f --- /dev/null +++ b/src/spac/templates/load_csv_files_with_config.py @@ -0,0 +1,98 @@ +""" +Platform-agnostic Load CSV Files template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.load_csv_files_with_config import run_from_json +>>> run_from_json("examples/load_csv_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, List, Optional +import pandas as pd +import logging + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.templates.template_utils import ( + save_outputs, + parse_params, + text_to_value, + load_csv_files +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], pd.DataFrame]: + """ + Execute Load CSV Files analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the DataFrame + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or DataFrame + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed DataFrame + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Extract parameters + csv_dir = Path(params["CSV_Files"]) + files_config = pd.read_csv(params["CSV_Files_Configuration"]) + string_columns = params.get("String_Columns", [""]) + + # Load and combine CSV files + final_df = load_csv_files( + csv_dir=csv_dir, + files_config=files_config, + string_columns=string_columns + ) + + if final_df is None: + raise RuntimeError("Failed to process CSV files") + + print("Load CSV Files completed successfully.") + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "combined_data.csv") + saved_files = save_outputs({output_file: final_df}) + + logging.info(f"Load CSV completed → {saved_files[output_file]}") + return saved_files + else: + # Return the DataFrame directly for in-memory workflows + logging.info("Returning DataFrame (not saving to file)") + return final_df + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python load_csv_template.py ", + file=sys.stderr + ) + sys.exit(1) + + saved_files = run_from_json(sys.argv[1]) + + if isinstance(saved_files, dict): + print("\nOutput files:") + for filename, filepath in saved_files.items(): + print(f" {filename}: {filepath}") \ No newline at end of file diff --git a/src/spac/templates/template_utils.py b/src/spac/templates/template_utils.py index 81f3c208..c36fbb04 100644 --- a/src/spac/templates/template_utils.py +++ b/src/spac/templates/template_utils.py @@ -2,10 +2,13 @@ import pickle from typing import Any, Dict, Union, Optional, List import json +import pandas as pd import anndata as ad +import re import logging logger = logging.getLogger(__name__) + def load_input(file_path: Union[str, Path]): """ Load input data from either h5ad or pickle file. @@ -108,7 +111,8 @@ def save_outputs( # Still support h5ad, but not the default if type(obj) is not ad.AnnData: raise TypeError( - f"Object for '{str(filename)}' must be AnnData, got {type(obj)}" + f"Object for '{str(filename)}' must be AnnData, " + f"got {type(obj)}" ) logger.info(f"Saving AnnData to {str(filepath)}") logger.debug(f"AnnData object: {obj}") @@ -357,3 +361,253 @@ def convert_pickle_to_h5ad( adata.write_h5ad(h5ad_path) return str(h5ad_path) + + +def spell_out_special_characters(text: str) -> str: + """ + Clean column names by replacing special characters with text equivalents. + + Handles biological marker names like: + - "CD4+" → "CD4_pos" + - "CD8-" → "CD8_neg" + - "CD4+CD20-" → "CD4_pos_CD20_neg" + - "CD4+/CD20-" → "CD4_pos_slashCD20_neg" + - "CD4+ CD20-" → "CD4_pos_CD20_neg" + - "Area µm²" → "Area_um2" + + Parameters + ---------- + text : str + The text to clean + + Returns + ------- + str + Cleaned text with special characters replaced + """ + # Replace spaces with underscores + text = text.replace(' ', '_') + + # Replace specific substrings for units + text = text.replace('µm²', 'um2') + text = text.replace('µm', 'um') + + # Handle hyphens between alphanumeric characters FIRST + # (before + and - replacements) + # This pattern matches a hyphen that has alphanumeric on both sides + text = re.sub(r'(?<=[A-Za-z0-9])-(?=[A-Za-z0-9])', '_', text) + + # Now replace remaining '+' with '_pos_' and '-' with '_neg_' + text = text.replace('+', '_pos_') + text = text.replace('-', '_neg_') + + # Mapping for specific characters + special_char_map = { + 'µ': 'u', # Micro symbol replaced with 'u' + '²': '2', # Superscript two replaced with '2' + '@': 'at', + '#': 'hash', + '$': 'dollar', + '%': 'percent', + '&': 'and', + '*': 'asterisk', + '/': 'slash', + '\\': 'backslash', + '=': 'equals', + '^': 'caret', + '!': 'exclamation', + '?': 'question', + '~': 'tilde', + '|': 'pipe', + ',': '', # Remove commas + '(': '', # Remove parentheses + ')': '', # Remove parentheses + '[': '', # Remove brackets + ']': '', # Remove brackets + '{': '', # Remove braces + '}': '', # Remove braces + } + + # Replace special characters using special_char_map + for char, replacement in special_char_map.items(): + text = text.replace(char, replacement) + + # Remove any remaining disallowed characters + # (keep only alphanumeric and underscore) + text = re.sub(r'[^a-zA-Z0-9_]', '', text) + + # Remove multiple consecutive underscores and + # replace with single underscore + text = re.sub(r'_+', '_', text) + + # Strip both leading and trailing underscores + text = text.strip('_') + + return text + + +def load_csv_files( + csv_dir: Union[str, Path], + files_config: pd.DataFrame, + string_columns: Optional[List[str]] = None +) -> pd.DataFrame: + """ + Load and combine CSV files based on configuration. + + Parameters + ---------- + csv_dir : str or Path + Directory containing CSV files + files_config : pd.DataFrame + Configuration dataframe with 'file_name' column and optional metadata + string_columns : list, optional + Columns to force as string type + + Returns + ------- + pd.DataFrame + Combined dataframe with all CSV data + """ + import pprint + from spac.data_utils import combine_dfs + from spac.utils import check_list_in_list + + csv_dir = Path(csv_dir) + filename = "file_name" + + # Clean configuration + files_config = files_config.map( + lambda x: x.strip() if isinstance(x, str) else x + ) + + # Get column names + all_column_names = files_config.columns.tolist() + filtered_column_names = [ + col for col in all_column_names if col not in [filename] + ] + + # Validate string_columns + if string_columns is None: + string_columns = [] + elif not isinstance(string_columns, list): + raise ValueError( + "String Columns must be a *list* of column names (strings)." + ) + + # Handle ["None"] or [""] => empty list + if (len(string_columns) == 1 and + isinstance(string_columns[0], str) and + text_to_value(string_columns[0]) is None): + string_columns = [] + + # Extract data types + dtypes = files_config.dtypes.to_dict() + + # Clean column names + def clean_column_name(column_name): + original = column_name + cleaned = spell_out_special_characters(column_name) + # Ensure doesn't start with digit + if cleaned and cleaned[0].isdigit(): + cleaned = f'col_{cleaned}' + if original != cleaned: + print(f'Column Name Updated: "{original}" -> "{cleaned}"') + return cleaned + + # Get files to process + files_config = files_config.astype(str) + files_to_use = [ + f.strip() for f in files_config[filename].tolist() + ] + + # Check all files exist + missing_files = [] + for file_name in files_to_use: + if not (csv_dir / file_name).exists(): + missing_files.append(file_name) + + if missing_files: + raise TypeError( + f"The following files are not found: {', '.join(missing_files)}" + ) + + # Prepare dtype override + dtype_override = {col: str for col in string_columns} if string_columns else None + + # Process files + processed_df_list = [] + first_file = True + + for file_name in files_to_use: + file_path = csv_dir / file_name + file_locations = files_config[ + files_config[filename] == file_name + ].index.tolist() + + # Check for duplicate file names + if len(file_locations) > 1: + print(f'Multiple entries for file: "{file_name}", exiting...') + return None + + try: + current_df = pd.read_csv(file_path, dtype=dtype_override) + print(f'\nProcessing file: "{file_name}"') + current_df.columns = [ + clean_column_name(col) for col in current_df.columns + ] + + # Validate string_columns exist + if first_file and string_columns: + check_list_in_list( + input=string_columns, + input_name='string_columns', + input_type='column', + target_list=list(current_df.columns), + need_exist=True, + warning=False + ) + first_file = False + + except pd.errors.EmptyDataError: + raise TypeError(f'The file: "{file_name}" is empty.') + except pd.errors.ParserError: + raise TypeError( + f'The file "{file_name}" could not be parsed. ' + 'Please check that the file is a valid CSV.' + ) + + current_df[filename] = file_name + + # Reorder columns + cols = current_df.columns.tolist() + cols.insert(0, cols.pop(cols.index(filename))) + current_df = current_df[cols] + + processed_df_list.append(current_df) + print(f'File: "{file_name}" Processed!\n') + + # Combine dataframes + final_df = combine_dfs(processed_df_list) + + # Ensure string columns remain strings + for col in string_columns: + if col in final_df.columns: + final_df[col] = final_df[col].astype(str) + + # Add metadata columns + if filtered_column_names: + for column in filtered_column_names: + # Map values from config + file_to_value = files_config.set_index(filename)[column].to_dict() + final_df[column] = final_df[filename].map(file_to_value) + # Ensure correct dtype + final_df[column] = final_df[column].astype(dtypes[column]) + + print(f'\n\nColumn "{column}" Mapping: ') + pp = pprint.PrettyPrinter(indent=4) + pp.pprint(file_to_value) + + print("\n\nFinal Dataframe Info") + print(final_df.info()) + + return final_df \ No newline at end of file diff --git a/tests/templates/test_load_csv_files_with_config.py b/tests/templates/test_load_csv_files_with_config.py new file mode 100644 index 00000000..7f845708 --- /dev/null +++ b/tests/templates/test_load_csv_files_with_config.py @@ -0,0 +1,275 @@ +# tests/templates/test_load_csv_template.py +"""Unit tests for the Load CSV Files template.""" + +import json +import os +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch, MagicMock + +import pandas as pd + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.load_csv_files_with_config import run_from_json + + +def create_mock_csv_files(tmp_dir: Path) -> tuple: + """Create minimal CSV files for testing.""" + # Create first CSV file + csv1_data = pd.DataFrame({ + 'CellID': [1, 2, 3], + 'X_centroid': [10.0, 20.0, 30.0], + 'Y_centroid': [15.0, 25.0, 35.0], + 'cell_type': ['TypeA', 'TypeB', 'TypeA'] + }) + csv1_path = tmp_dir / 'sample1.csv' + csv1_data.to_csv(csv1_path, index=False) + + # Create second CSV file + csv2_data = pd.DataFrame({ + 'CellID': [4, 5, 6], + 'X_centroid': [40.0, 50.0, 60.0], + 'Y_centroid': [45.0, 55.0, 65.0], + 'cell_type': ['TypeB', 'TypeB', 'TypeA'] + }) + csv2_path = tmp_dir / 'sample2.csv' + csv2_data.to_csv(csv2_path, index=False) + + # Create configuration file + config_data = pd.DataFrame({ + 'file_name': ['sample1.csv', 'sample2.csv'], + 'slide_number': ['S1', 'S2'] + }) + config_path = tmp_dir / 'config.csv' + config_data.to_csv(config_path, index=False) + + return csv1_path, csv2_path, config_path + + +class TestLoadCSVFilesWithConfig(unittest.TestCase): + """Unit tests for the Load CSV Files template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.tmp_path = Path(self.tmp_dir.name) + + # Create mock CSV files + self.csv1, self.csv2, self.config = create_mock_csv_files( + self.tmp_path + ) + + # Minimal parameters + self.params = { + "CSV_Files": str(self.tmp_path), + "CSV_Files_Configuration": str(self.config), + "String_Columns": [""], + "Output_File": "combined.csv" + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering all input/output scenarios.""" + # Test 1: Run with save_results=True + saved_files = run_from_json(self.params) + self.assertIn("combined.csv", saved_files) + output_path = Path(saved_files["combined.csv"]) + self.assertTrue(output_path.exists()) + + # Verify content + result_df = pd.read_csv(output_path) + self.assertEqual(len(result_df), 6) # 3 + 3 rows + self.assertIn('file_name', result_df.columns) + self.assertIn('slide_number', result_df.columns) + self.assertIn('CellID', result_df.columns) + + # Test 2: Run with save_results=False + df_result = run_from_json(self.params, save_results=False) + self.assertIsInstance(df_result, pd.DataFrame) + self.assertEqual(len(df_result), 6) + + # Test 3: JSON file input + json_path = self.tmp_path / "params.json" + with open(json_path, "w") as f: + json.dump(self.params, f) + saved_from_json = run_from_json(json_path) + self.assertIn("combined.csv", saved_from_json) + + # Test 4: String columns specified + params_with_strings = self.params.copy() + params_with_strings["String_Columns"] = ["CellID"] + df_with_strings = run_from_json(params_with_strings, save_results=False) + self.assertEqual(df_with_strings['CellID'].dtype, 'object') + + # Test 5: Special characters in column names + special_csv = pd.DataFrame({ + 'Cell-ID': [1, 2], + 'Area µm²': [100.0, 200.0], + 'CD4+': ['pos', 'neg'] + }) + special_path = self.tmp_path / 'special.csv' + special_csv.to_csv(special_path, index=False) + + special_config = pd.DataFrame({ + 'file_name': ['special.csv'], + 'experiment': ['Exp1'] + }) + special_config_path = self.tmp_path / 'special_config.csv' + special_config.to_csv(special_config_path, index=False) + + params_special = { + "CSV_Files": str(self.tmp_path), + "CSV_Files_Configuration": str(special_config_path), + "String_Columns": [""] + } + df_special = run_from_json(params_special, save_results=False) + # Check column names were cleaned + self.assertIn('Cell_ID', df_special.columns) + self.assertIn('Area_um2', df_special.columns) + self.assertIn('CD4_pos', df_special.columns) + + def test_error_messages(self) -> None: + """Test exact error messages for various failure scenarios.""" + # Test 1: Missing CSV file + bad_config = pd.DataFrame({ + 'file_name': ['missing.csv'], + 'slide_number': ['S1'] + }) + bad_config_path = self.tmp_path / 'bad_config.csv' + bad_config.to_csv(bad_config_path, index=False) + + params_missing = self.params.copy() + params_missing["CSV_Files_Configuration"] = str(bad_config_path) + + with self.assertRaises(TypeError) as context: + run_from_json(params_missing) + expected_msg = "The following files are not found: missing.csv" + self.assertEqual(expected_msg, str(context.exception)) + + # Test 2: Empty CSV file + empty_path = self.tmp_path / 'empty.csv' + empty_path.write_text('') + + empty_config = pd.DataFrame({ + 'file_name': ['empty.csv'], + 'slide_number': ['S1'] + }) + empty_config_path = self.tmp_path / 'empty_config.csv' + empty_config.to_csv(empty_config_path, index=False) + + params_empty = self.params.copy() + params_empty["CSV_Files_Configuration"] = str(empty_config_path) + + with self.assertRaises(TypeError) as context: + run_from_json(params_empty) + expected_msg = 'The file: "empty.csv" is empty.' + self.assertEqual(expected_msg, str(context.exception)) + + # Test 3: Invalid CSV file + invalid_path = self.tmp_path / 'invalid.csv' + # Create a truly invalid CSV that will cause parser error + invalid_path.write_text('col1,col2,col3\n"unclosed quote,value2,value3\nvalue4,value5') + + invalid_config = pd.DataFrame({ + 'file_name': ['invalid.csv'], + 'slide_number': ['S1'] + }) + invalid_config_path = self.tmp_path / 'invalid_config.csv' + invalid_config.to_csv(invalid_config_path, index=False) + + params_invalid = self.params.copy() + params_invalid["CSV_Files_Configuration"] = str(invalid_config_path) + + with self.assertRaises(TypeError) as context: + run_from_json(params_invalid) + expected_msg = ( + 'The file "invalid.csv" could not be parsed. ' + 'Please check that the file is a valid CSV.' + ) + self.assertEqual(expected_msg, str(context.exception)) + + # Test 4: Invalid string_columns parameter + params_bad_strings = self.params.copy() + params_bad_strings["String_Columns"] = "not_a_list" + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad_strings) + expected_msg = ( + "String Columns must be a *list* of column names (strings)." + ) + self.assertEqual(expected_msg, str(context.exception)) + + def test_metadata_mapping(self) -> None: + """Test that metadata columns are correctly mapped.""" + df_result = run_from_json(self.params, save_results=False) + + # Check slide_number mapping + sample1_rows = df_result[df_result['file_name'] == 'sample1.csv'] + sample2_rows = df_result[df_result['file_name'] == 'sample2.csv'] + + self.assertTrue(all(sample1_rows['slide_number'] == 'S1')) + self.assertTrue(all(sample2_rows['slide_number'] == 'S2')) + + @patch('builtins.print') + def test_console_output(self, mock_print) -> None: + """Test that progress is printed to console.""" + run_from_json(self.params, save_results=False) + + # Check for expected print statements + print_calls = [str(call[0][0]) for call in mock_print.call_args_list + if call[0]] + + # Should print processing messages + processing_msgs = [msg for msg in print_calls + if 'Processing file:' in msg] + self.assertEqual(len(processing_msgs), 2) # Two files + + # Should print completion message + completion_msgs = [msg for msg in print_calls + if 'Load CSV Files completed' in msg] + self.assertTrue(len(completion_msgs) > 0) + + def test_duplicate_file_handling(self) -> None: + """Test handling of duplicate file names in config.""" + # Create config with duplicate entries + dup_config = pd.DataFrame({ + 'file_name': ['sample1.csv', 'sample1.csv'], + 'slide_number': ['S1', 'S2'] + }) + dup_config_path = self.tmp_path / 'dup_config.csv' + dup_config.to_csv(dup_config_path, index=False) + + params_dup = self.params.copy() + params_dup["CSV_Files_Configuration"] = str(dup_config_path) + + with self.assertRaises(RuntimeError) as context: + run_from_json(params_dup) + self.assertIn( + "Failed to process CSV files", + str(context.exception) + ) + + def test_string_columns_validation(self) -> None: + """Test validation of string_columns parameter.""" + # Test non-existent column + params_bad_col = self.params.copy() + params_bad_col["String_Columns"] = ["NonExistentColumn"] + + with self.assertRaises(ValueError): + run_from_json(params_bad_col) + + # Test None handling + params_none = self.params.copy() + params_none["String_Columns"] = ["None"] + df_none = run_from_json(params_none, save_results=False) + self.assertIsInstance(df_none, pd.DataFrame) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/templates/test_template_utils.py b/tests/templates/test_template_utils.py index 886b33d8..b8548bb1 100644 --- a/tests/templates/test_template_utils.py +++ b/tests/templates/test_template_utils.py @@ -8,7 +8,7 @@ import tempfile import unittest import warnings - +from unittest.mock import patch import anndata as ad import numpy as np import pandas as pd @@ -23,7 +23,9 @@ save_outputs, text_to_value, convert_pickle_to_h5ad, - convert_to_floats + convert_to_floats, + spell_out_special_characters, + load_csv_files ) @@ -292,6 +294,230 @@ def test_default_pickle_extension(self) -> None: self.assertTrue(filepath.endswith('.pickle')) self.assertTrue(os.path.exists(filepath)) + def test_spell_out_special_characters(self) -> None: + """Test spell_out_special_characters function.""" + from spac.templates.template_utils import spell_out_special_characters + + # Test space replacement + result = spell_out_special_characters("Cell Type") + self.assertEqual(result, "Cell_Type") + + # Test special units + result = spell_out_special_characters("Area µm²") + self.assertEqual(result, "Area_um2") + + # Test hyphen between letters + result = spell_out_special_characters("CD4-positive") + self.assertEqual(result, "CD4_positive") + + # Test plus/minus + result = spell_out_special_characters("CD4+") + self.assertEqual(result, "CD4_pos") # Trailing underscore is stripped + result = spell_out_special_characters("CD8-") + self.assertEqual(result, "CD8_neg") # Trailing underscore is stripped + + # Test combination markers + result = spell_out_special_characters("CD4+CD20-") + self.assertEqual(result, "CD4_pos_CD20_neg") + + # Test edge cases with special separators + result = spell_out_special_characters("CD4+/CD20-") + self.assertEqual(result, "CD4_pos_slashCD20_neg") + + result = spell_out_special_characters("CD4+ CD20-") + self.assertEqual(result, "CD4_pos_CD20_neg") + + result = spell_out_special_characters("CD4+,CD20-") + self.assertEqual(result, "CD4_pos_CD20_neg") + + # Test parentheses removal + result = spell_out_special_characters("CD4+ (bright)") + self.assertEqual(result, "CD4_pos_bright") + + # Test special characters + result = spell_out_special_characters("Cell@100%") + self.assertEqual(result, "Cellat100percent") + + # Test multiple underscores + result = spell_out_special_characters("Cell___Type") + self.assertEqual(result, "Cell_Type") + + # Test leading/trailing underscores + result = spell_out_special_characters("_Cell_Type_") + self.assertEqual(result, "Cell_Type") + + # Test complex case + result = spell_out_special_characters("CD4+ T-cells (µm²)") + self.assertEqual(result, "CD4_pos_T_cells_um2") + + # Test empty string + result = spell_out_special_characters("") + self.assertEqual(result, "") + + # Additional edge cases + result = spell_out_special_characters("CD3+CD4+CD8-") + self.assertEqual(result, "CD3_pos_CD4_pos_CD8_neg") + + result = spell_out_special_characters("PD-1/PD-L1") + self.assertEqual(result, "PD_1slashPD_L1") + + result = spell_out_special_characters("CD45RA+CD45RO-") + self.assertEqual(result, "CD45RA_pos_CD45RO_neg") + + result = spell_out_special_characters("CD4+CD25+FOXP3+") + self.assertEqual(result, "CD4_pos_CD25_pos_FOXP3_pos") + + # Test with multiple special characters + result = spell_out_special_characters("CD4+ & CD8+ (double positive)") + self.assertEqual(result, "CD4_pos_and_CD8_pos_double_positive") + + # Test with numbers at start (should add col_ prefix in + # clean_column_name) + result = spell_out_special_characters("123ABC") + # Note: col_ prefix is added by clean_column_name + self.assertEqual(result, "123ABC") + + def test_load_csv_files(self) -> None: + """Test load_csv_files function.""" + + # Create test CSV files + csv_dir = Path(self.tmp_dir.name) / "csv_data" + csv_dir.mkdir() + + # CSV 1: Normal data + csv1 = pd.DataFrame({ + 'ID': ['001', '002', '003'], + 'Value': [1.5, 2.5, 3.5], + 'Type': ['A', 'B', 'A'] + }) + csv1.to_csv(csv_dir / 'data1.csv', index=False) + + # CSV 2: Special characters in columns + csv2 = pd.DataFrame({ + 'ID': ['004', '005'], + 'Value': [4.5, 5.5], + 'Type': ['B', 'C'], + 'Area µm²': [100, 200] + }) + csv2.to_csv(csv_dir / 'data2.csv', index=False) + + # Test 1: Basic loading with metadata + config = pd.DataFrame({ + 'file_name': ['data1.csv', 'data2.csv'], + 'experiment': ['Exp1', 'Exp2'], + 'batch': [1, 2] + }) + + result = load_csv_files(csv_dir, config) + + # Verify basic structure + self.assertEqual(len(result), 5) # 3 + 2 rows + self.assertIn('file_name', result.columns) + self.assertIn('experiment', result.columns) + self.assertIn('batch', result.columns) + self.assertIn('ID', result.columns) + self.assertIn('Area_um2', result.columns) # Cleaned name + + # Verify metadata mapping + exp1_rows = result[result['file_name'] == 'data1.csv'] + self.assertTrue(all(exp1_rows['experiment'] == 'Exp1')) + self.assertTrue(all(exp1_rows['batch'] == 1)) + + # Test 2: String columns preservation + result_str = load_csv_files( + csv_dir, config, string_columns=['ID'] + ) + self.assertEqual(result_str['ID'].dtype, 'object') + self.assertTrue(all(isinstance(x, str) for x in result_str['ID'])) + + # Test 3: Empty string_columns list + result_empty = load_csv_files(csv_dir, config, string_columns=[]) + self.assertIsInstance(result_empty, pd.DataFrame) + + # Test 4: Column name with spaces in config + config_spaces = pd.DataFrame({ + 'file_name': ['data1.csv'], + 'Sample Type': ['Control'] # Space in column name + }) + with self.assertRaises(ValueError): + # Should fail validation due to string_columns not being list + load_csv_files(csv_dir, config_spaces, string_columns="ID") + + # Test 5: Missing file in config + config_missing = pd.DataFrame({ + 'file_name': ['missing.csv'], + 'experiment': ['Exp3'] + }) + with self.assertRaises(TypeError) as context: + load_csv_files(csv_dir, config_missing) + self.assertIn("not found", str(context.exception)) + + # Test 6: Empty CSV file + empty_csv = csv_dir / 'empty.csv' + empty_csv.write_text('') + config_empty = pd.DataFrame({ + 'file_name': ['empty.csv'], + 'experiment': ['Exp4'] + }) + with self.assertRaises(TypeError) as context: + load_csv_files(csv_dir, config_empty) + self.assertIn("empty", str(context.exception)) + + # Test 7: First file validation for string_columns + config_single = pd.DataFrame({ + 'file_name': ['data1.csv'] + }) + with self.assertRaises(ValueError): + # Non-existent column should raise error + load_csv_files( + csv_dir, config_single, + string_columns=['NonExistentColumn'] + ) + + @patch('builtins.print') + def test_load_csv_files_console_output(self, mock_print) -> None: + """Test console output from load_csv_files.""" + from spac.templates.template_utils import load_csv_files + + # Setup test data + csv_dir = Path(self.tmp_dir.name) / "csv_test" + csv_dir.mkdir() + + csv_data = pd.DataFrame({ + 'ID': [1, 2], + 'CD4+': ['pos', 'neg'] # Special character + }) + csv_data.to_csv(csv_dir / 'test.csv', index=False) + + config = pd.DataFrame({ + 'file_name': ['test.csv'], + 'group': ['A'] + }) + + # Run function + load_csv_files(csv_dir, config) + + # Check console output + print_calls = [str(call[0][0]) for call in mock_print.call_args_list + if call[0]] + + # Should print column name updates + updates = [msg for msg in print_calls + if 'Column Name Updated:' in msg] + self.assertTrue(len(updates) > 0) + # The function strips trailing underscores, so CD4+ becomes CD4_pos + self.assertTrue(any('CD4+' in msg and 'CD4_pos' in msg + for msg in updates)) + + # Should print processing messages + processing = [msg for msg in print_calls + if 'Processing file:' in msg] + self.assertTrue(len(processing) > 0) + + # Should print final info + final_info = [msg for msg in print_calls + if 'Final Dataframe Info' in msg] + self.assertTrue(len(final_info) > 0) if __name__ == "__main__": - unittest.main() + unittest.main() \ No newline at end of file From bbfa2f6cc82aadaf67b904642ec3f0ff61b3a816 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sun, 27 Jul 2025 21:36:06 -0400 Subject: [PATCH 021/102] fix(template_utils): Use applymap instead of map for pandas compatibility --- src/spac/templates/template_utils.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/spac/templates/template_utils.py b/src/spac/templates/template_utils.py index c36fbb04..de42ebaa 100644 --- a/src/spac/templates/template_utils.py +++ b/src/spac/templates/template_utils.py @@ -459,7 +459,8 @@ def load_csv_files( csv_dir : str or Path Directory containing CSV files files_config : pd.DataFrame - Configuration dataframe with 'file_name' column and optional metadata + Configuration dataframe with 'file_name' column and optional + metadata string_columns : list, optional Columns to force as string type @@ -476,7 +477,7 @@ def load_csv_files( filename = "file_name" # Clean configuration - files_config = files_config.map( + files_config = files_config.applymap( lambda x: x.strip() if isinstance(x, str) else x ) @@ -528,11 +529,14 @@ def clean_column_name(column_name): if missing_files: raise TypeError( - f"The following files are not found: {', '.join(missing_files)}" + f"The following files are not found: " + f"{', '.join(missing_files)}" ) # Prepare dtype override - dtype_override = {col: str for col in string_columns} if string_columns else None + dtype_override = ( + {col: str for col in string_columns} if string_columns else None + ) # Process files processed_df_list = [] @@ -546,7 +550,9 @@ def clean_column_name(column_name): # Check for duplicate file names if len(file_locations) > 1: - print(f'Multiple entries for file: "{file_name}", exiting...') + print( + f'Multiple entries for file: "{file_name}", exiting...' + ) return None try: @@ -598,7 +604,9 @@ def clean_column_name(column_name): if filtered_column_names: for column in filtered_column_names: # Map values from config - file_to_value = files_config.set_index(filename)[column].to_dict() + file_to_value = ( + files_config.set_index(filename)[column].to_dict() + ) final_df[column] = final_df[filename].map(file_to_value) # Ensure correct dtype final_df[column] = final_df[column].astype(dtypes[column]) From 1cfb39ed56e30e805bf7bcf29399e8ce0f20333c Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:08:25 -0400 Subject: [PATCH 022/102] feat(setup_analysis_template): add setup_analysis_template function and unit tests --- src/spac/templates/setup_analysis_template.py | 158 ++++++++++ .../templates/test_setup_analysis_template.py | 298 ++++++++++++++++++ 2 files changed, 456 insertions(+) create mode 100644 src/spac/templates/setup_analysis_template.py create mode 100644 tests/templates/test_setup_analysis_template.py diff --git a/src/spac/templates/setup_analysis_template.py b/src/spac/templates/setup_analysis_template.py new file mode 100644 index 00000000..1fa79eb5 --- /dev/null +++ b/src/spac/templates/setup_analysis_template.py @@ -0,0 +1,158 @@ +""" +Platform-agnostic Setup Analysis template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.setup_analysis_template import run_from_json +>>> run_from_json("examples/setup_analysis_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, List, Optional +import pandas as pd +import logging + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.data_utils import ingest_cells +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], Any]: + """ + Execute Setup Analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the adata object + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or AnnData + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed AnnData object + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Extract parameters + upstream_dataset = params["Upstream_Dataset"] + feature_names = params["Features_to_Analyze"] + regex_str = params.get("Feature_Regex", []) + x_col = params["X_Coordinate_Column"] + y_col = params["Y_Coordinate_Column"] + annotation = params["Annotation_s_"] + + # Load upstream data - could be DataFrame or CSV + if isinstance(upstream_dataset, (str, Path)): + # Check if it's a pickle/h5ad file or CSV + path = Path(upstream_dataset) + if path.suffix.lower() in ['.pickle', '.pkl', '.p', '.h5ad']: + input_dataset = load_input(upstream_dataset) + else: + # Assume it's a CSV file + input_dataset = pd.read_csv(upstream_dataset) + else: + # Already a DataFrame + input_dataset = upstream_dataset + + # Process annotation parameter + if isinstance(annotation, str): + annotation = [annotation] + + if len(annotation) == 1 and annotation[0] == "None": + annotation = None + + if annotation and len(annotation) != 1 and "None" in annotation: + error_msg = 'String "None" found in the annotation list' + raise ValueError(error_msg) + + # Process coordinate columns + x_col = text_to_value(x_col, default_none_text="None") + y_col = text_to_value(y_col, default_none_text="None") + + # Process feature names and regex + if isinstance(feature_names, str): + feature_names = [feature_names] + if isinstance(regex_str, str): + import ast + try: + regex_str = ast.literal_eval(regex_str) + except (ValueError, SyntaxError): + regex_str = [regex_str] if regex_str else [] + + # Processing two search methods + for feature in feature_names: + regex_str.append(f"^{feature}$") + + # Sanitizing search list + regex_str_set = set(regex_str) + regex_str_list = list(regex_str_set) + + # Run the ingestion + ingested_anndata = ingest_cells( + dataframe=input_dataset, + regex_str=regex_str_list, + x_col=x_col, + y_col=y_col, + annotation=annotation + ) + + print("Analysis Setup:") + print(ingested_anndata) + print("Schema:") + print(ingested_anndata.var_names) + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "transform_output.pickle") + # Default to pickle format + if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): + output_file = output_file.replace('.h5ad', '.pickle') + + saved_files = save_outputs({output_file: ingested_anndata}) + + logging.info( + f"Setup Analysis completed → {saved_files[output_file]}" + ) + return saved_files + else: + # Return the adata object directly for in-memory workflows + logging.info("Returning AnnData object (not saving to file)") + return ingested_anndata + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python setup_analysis_template.py ", + file=sys.stderr + ) + sys.exit(1) + + saved_files = run_from_json(sys.argv[1]) + + if isinstance(saved_files, dict): + print("\nOutput files:") + for filename, filepath in saved_files.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned AnnData object") \ No newline at end of file diff --git a/tests/templates/test_setup_analysis_template.py b/tests/templates/test_setup_analysis_template.py new file mode 100644 index 00000000..6fd2e056 --- /dev/null +++ b/tests/templates/test_setup_analysis_template.py @@ -0,0 +1,298 @@ +# tests/templates/test_setup_analysis_template.py +"""Unit tests for the Setup Analysis template.""" + +import json +import os +import sys +import tempfile +import unittest +import pickle +from pathlib import Path +import pandas as pd +import anndata as ad +import numpy as np +from unittest.mock import patch, MagicMock + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.setup_analysis_template import run_from_json + + +def create_test_dataframe(n_cells: int = 10) -> pd.DataFrame: + """Create minimal test dataframe for setup analysis.""" + rng = np.random.default_rng(0) + df = pd.DataFrame({ + 'CellID': range(1, n_cells + 1), + 'X_centroid': rng.uniform(0, 100, n_cells), + 'Y_centroid': rng.uniform(0, 100, n_cells), + 'CD25': rng.normal(10, 2, n_cells), + 'CD3D': rng.normal(15, 3, n_cells), + 'CD45': rng.normal(20, 4, n_cells), + 'CD4': rng.normal(12, 2.5, n_cells), + 'CD8A': rng.normal(8, 2, n_cells), + 'broad_cell_type': rng.choice( + ['T cells', 'B cells'], n_cells + ), + 'detailed_cell_type': rng.choice( + ['CD4 T cells', 'CD8 T cells', 'B cells'], n_cells + ) + }) + return df + + +class TestSetupAnalysisTemplate(unittest.TestCase): + """Unit tests for the Setup Analysis template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + + # Create test data + self.test_df = create_test_dataframe() + self.csv_file = os.path.join( + self.tmp_dir.name, "test_data.csv" + ) + self.test_df.to_csv(self.csv_file, index=False) + + # Minimal parameters + self.params = { + "Upstream_Dataset": self.csv_file, + "Features_to_Analyze": ["CD25", "CD3D", "CD45", "CD4", "CD8A"], + "Feature_Regex": [], + "X_Coordinate_Column": "X_centroid", + "Y_Coordinate_Column": "Y_centroid", + "Annotation_s_": ["broad_cell_type", "detailed_cell_type"], + "Output_File": "analysis_output.pickle" + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + @patch('spac.templates.setup_analysis_template.ingest_cells') + def test_run_with_save(self, mock_ingest) -> None: + """Test setup analysis with file saving.""" + # Mock the ingest_cells function + mock_adata = ad.AnnData( + X=np.random.rand(10, 5), + obs=pd.DataFrame({'cell_type': ['A'] * 10}) + ) + mock_adata.var_names = ['CD25', 'CD3D', 'CD45', 'CD4', 'CD8A'] + mock_ingest.return_value = mock_adata + + saved_files = run_from_json(self.params) + + # Check that ingest_cells was called with correct parameters + mock_ingest.assert_called_once() + call_args = mock_ingest.call_args + + # Verify the call arguments + self.assertIsInstance(call_args[1]['dataframe'], pd.DataFrame) + self.assertEqual( + call_args[1]['x_col'], "X_centroid" + ) + self.assertEqual( + call_args[1]['y_col'], "Y_centroid" + ) + self.assertEqual( + call_args[1]['annotation'], + ["broad_cell_type", "detailed_cell_type"] + ) + + # Check regex includes feature names + regex_list = call_args[1]['regex_str'] + for feature in self.params["Features_to_Analyze"]: + self.assertIn(f"^{feature}$", regex_list) + + # Check that output file was created + self.assertIn("analysis_output.pickle", saved_files) + + @patch('spac.templates.setup_analysis_template.ingest_cells') + def test_run_without_save(self, mock_ingest) -> None: + """Test setup analysis without file saving.""" + # Mock the ingest_cells function + mock_adata = ad.AnnData( + X=np.random.rand(10, 5), + obs=pd.DataFrame({'cell_type': ['A'] * 10}) + ) + mock_adata.var_names = ['CD25', 'CD3D', 'CD45', 'CD4', 'CD8A'] + mock_ingest.return_value = mock_adata + + result = run_from_json(self.params, save_results=False) + + # Check that we got an AnnData object back + self.assertIsInstance(result, ad.AnnData) + # For AnnData, we check that it's the same object reference + self.assertIs(result, mock_adata) + + def test_annotation_none_handling(self) -> None: + """Test handling of 'None' annotation.""" + params_none = self.params.copy() + params_none["Annotation_s_"] = ["None"] + + with patch( + 'spac.templates.setup_analysis_template.ingest_cells' + ) as mock_ingest: + mock_adata = ad.AnnData(X=np.random.rand(10, 5)) + mock_adata.var_names = ['CD25', 'CD3D', 'CD45', 'CD4', 'CD8A'] + mock_ingest.return_value = mock_adata + + run_from_json(params_none, save_results=False) + + # Check that annotation was set to None + call_args = mock_ingest.call_args + self.assertIsNone(call_args[1]['annotation']) + + def test_annotation_validation_error_message(self) -> None: + """Test exact error message for invalid annotation.""" + params_bad = self.params.copy() + params_bad["Annotation_s_"] = ["broad_cell_type", "None", "other"] + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + expected_msg = 'String "None" found in the annotation list' + actual_msg = str(context.exception) + self.assertEqual(expected_msg, actual_msg) + + def test_coordinate_none_handling(self) -> None: + """Test handling of 'None' coordinates.""" + params_no_coords = self.params.copy() + params_no_coords["X_Coordinate_Column"] = "None" + params_no_coords["Y_Coordinate_Column"] = "None" + + with patch( + 'spac.templates.setup_analysis_template.ingest_cells' + ) as mock_ingest: + mock_adata = ad.AnnData(X=np.random.rand(10, 5)) + mock_adata.var_names = ['CD25', 'CD3D', 'CD45', 'CD4', 'CD8A'] + mock_ingest.return_value = mock_adata + + run_from_json(params_no_coords, save_results=False) + + # Check that coordinates were set to None + call_args = mock_ingest.call_args + self.assertIsNone(call_args[1]['x_col']) + self.assertIsNone(call_args[1]['y_col']) + + def test_json_file_input(self) -> None: + """Test with JSON file input.""" + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + with patch( + 'spac.templates.setup_analysis_template.ingest_cells' + ) as mock_ingest: + mock_adata = ad.AnnData(X=np.random.rand(10, 5)) + mock_adata.var_names = ['CD25', 'CD3D', 'CD45', 'CD4', 'CD8A'] + mock_ingest.return_value = mock_adata + + saved_files = run_from_json(json_path) + + # Check that files were saved + self.assertTrue(len(saved_files) > 0) + + def test_feature_regex_combination(self) -> None: + """Test combination of feature names and regex.""" + params_regex = self.params.copy() + params_regex["Feature_Regex"] = [".*_expression$", "DAPI.*"] + + with patch( + 'spac.templates.setup_analysis_template.ingest_cells' + ) as mock_ingest: + mock_adata = ad.AnnData(X=np.random.rand(10, 5)) + mock_adata.var_names = ['CD25', 'CD3D', 'CD45', 'CD4', 'CD8A'] + mock_ingest.return_value = mock_adata + + run_from_json(params_regex, save_results=False) + + # Check that regex includes both custom patterns and features + call_args = mock_ingest.call_args + regex_list = call_args[1]['regex_str'] + + # Should include custom regex + self.assertIn(".*_expression$", regex_list) + self.assertIn("DAPI.*", regex_list) + + # Should include feature patterns + for feature in params_regex["Features_to_Analyze"]: + self.assertIn(f"^{feature}$", regex_list) + + def test_dataframe_input(self) -> None: + """Test with DataFrame as upstream dataset.""" + params_df = self.params.copy() + params_df["Upstream_Dataset"] = self.test_df + + with patch( + 'spac.templates.setup_analysis_template.ingest_cells' + ) as mock_ingest: + mock_adata = ad.AnnData(X=np.random.rand(10, 5)) + mock_adata.var_names = ['CD25', 'CD3D', 'CD45', 'CD4', 'CD8A'] + mock_ingest.return_value = mock_adata + + run_from_json(params_df, save_results=False) + + # Check that DataFrame was passed directly + call_args = mock_ingest.call_args + pd.testing.assert_frame_equal( + call_args[1]['dataframe'], self.test_df + ) + + @patch('builtins.print') + def test_console_output(self, mock_print) -> None: + """Test console output messages.""" + with patch( + 'spac.templates.setup_analysis_template.ingest_cells' + ) as mock_ingest: + mock_adata = ad.AnnData(X=np.random.rand(10, 5)) + mock_adata.var_names = ['CD25', 'CD3D', 'CD45', 'CD4', 'CD8A'] + mock_ingest.return_value = mock_adata + + run_from_json(self.params, save_results=False) + + # Verify output messages + print_calls = [ + str(call[0][0]) for call in mock_print.call_args_list + if call[0] + ] + + # Should print "Analysis Setup:" + setup_msgs = [ + msg for msg in print_calls + if 'Analysis Setup:' in msg + ] + self.assertTrue(len(setup_msgs) > 0) + + # Should print "Schema:" + schema_msgs = [ + msg for msg in print_calls + if 'Schema:' in msg + ] + self.assertTrue(len(schema_msgs) > 0) + + def test_single_feature_and_annotation(self) -> None: + """Test with single feature and annotation as strings.""" + params_single = self.params.copy() + params_single["Features_to_Analyze"] = "CD25" + params_single["Annotation_s_"] = "broad_cell_type" + + with patch( + 'spac.templates.setup_analysis_template.ingest_cells' + ) as mock_ingest: + mock_adata = ad.AnnData(X=np.random.rand(10, 1)) + mock_adata.var_names = ['CD25'] + mock_ingest.return_value = mock_adata + + run_from_json(params_single, save_results=False) + + # Check that single values were converted to lists + call_args = mock_ingest.call_args + self.assertIn("^CD25$", call_args[1]['regex_str']) + self.assertEqual( + call_args[1]['annotation'], ["broad_cell_type"] + ) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From dddc33a78a0d6af0262eefd4b9250c0cfc19b77e Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:28:00 -0400 Subject: [PATCH 023/102] fix(setup_analysis_template): fix setup_analysis_template function --- src/spac/templates/setup_analysis_template.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/spac/templates/setup_analysis_template.py b/src/spac/templates/setup_analysis_template.py index 1fa79eb5..298e6d32 100644 --- a/src/spac/templates/setup_analysis_template.py +++ b/src/spac/templates/setup_analysis_template.py @@ -7,11 +7,12 @@ >>> from spac.templates.setup_analysis_template import run_from_json >>> run_from_json("examples/setup_analysis_params.json") """ -import json + import sys from pathlib import Path -from typing import Any, Dict, Union, List, Optional +from typing import Any, Dict, Union import pandas as pd +import ast import logging # Add parent directory to path for imports @@ -91,7 +92,6 @@ def run_from_json( if isinstance(feature_names, str): feature_names = [feature_names] if isinstance(regex_str, str): - import ast try: regex_str = ast.literal_eval(regex_str) except (ValueError, SyntaxError): @@ -123,9 +123,9 @@ def run_from_json( if save_results: # Save outputs output_file = params.get("Output_File", "transform_output.pickle") - # Default to pickle format + # Default to pickle format if no recognized extension if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): - output_file = output_file.replace('.h5ad', '.pickle') + output_file = output_file + '.pickle' saved_files = save_outputs({output_file: ingested_anndata}) From eb810ab4a532901817a5652ad59733af38b083fd Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 28 Jul 2025 14:41:26 -0400 Subject: [PATCH 024/102] feat(boxplot_template): add boxplot_template function and unit tests --- src/spac/templates/boxplot_template.py | 181 ++++++++++ tests/templates/test_boxplot_template.py | 422 +++++++++++++++++++++++ 2 files changed, 603 insertions(+) create mode 100644 src/spac/templates/boxplot_template.py create mode 100644 tests/templates/test_boxplot_template.py diff --git a/src/spac/templates/boxplot_template.py b/src/spac/templates/boxplot_template.py new file mode 100644 index 00000000..009ce286 --- /dev/null +++ b/src/spac/templates/boxplot_template.py @@ -0,0 +1,181 @@ +""" +Platform-agnostic Boxplot template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.boxplot_template import run_from_json +>>> run_from_json("examples/boxplot_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, List, Optional, Tuple +import logging +import pandas as pd +import matplotlib.pyplot as plt +import seaborn as sns + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.visualization import boxplot +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True, + show_plot: bool = True +) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: + """ + Execute Boxplot analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the figure and + summary dataframe directly for in-memory workflows. Default is True. + show_plot : bool, optional + Whether to display the plot. Default is True. + + Returns + ------- + dict or tuple + If save_results=True: Dictionary of saved file paths + If save_results=False: Tuple of (figure, summary_dataframe) + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + annotation = params.get("Primary_Annotation", "None") + second_annotation = params.get("Secondary_Annotation", "None") + layer_to_plot = params.get("Table_to_Visualize", "Original") + feature_to_plot = params.get("Feature_s_to_Plot", ["All"]) + log_scale = params.get("Value_Axis_Log_Scale", False) + + # Figure parameters + figure_title = params.get("Figure_Title", "BoxPlot") + figure_horizontal = params.get("Horizontal_Plot", False) + fig_width = params.get("Figure_Width", 12) + fig_height = params.get("Figure_Height", 8) + fig_dpi = params.get("Figure_DPI", 300) + font_size = params.get("Font_Size", 10) + showfliers = params.get("Keep_Outliers", True) + + # Process parameters exactly as in NIDAP template + if layer_to_plot == "Original": + layer_to_plot = None + + if second_annotation == "None": + second_annotation = None + + if annotation == "None": + annotation = None + + if figure_horizontal: + figure_orientation = "h" + else: + figure_orientation = "v" + + if any(item == "All" for item in feature_to_plot): + logging.info("Plotting All Features") + feature_to_plot = adata.var_names.tolist() + else: + feature_str = "\n".join(feature_to_plot) + logging.info(f"Plotting Feature:\n{feature_str}") + + # Create the plot exactly as in NIDAP template + fig, ax = plt.subplots() + plt.rcParams.update({'font.size': font_size}) + fig.set_size_inches(fig_width, fig_height) + fig.set_dpi(fig_dpi) + + fig, ax, df = boxplot( + adata=adata, + ax=ax, + layer=layer_to_plot, + annotation=annotation, + second_annotation=second_annotation, + features=feature_to_plot, + log_scale=log_scale, + orient=figure_orientation, + showfliers=showfliers + ) + + # Set the figure title + ax.set_title(figure_title) + + # Get summary statistics of the dataset + logging.info("Summary statistics of the dataset:") + summary = df.describe() + + # Convert the summary to a DataFrame that includes the index as a column + summary_df = summary.reset_index() + logging.info(f"\n{summary_df.to_string()}") + + # Move the legend outside the plotting area + # Check if a legend exists + try: + sns.move_legend(ax, "upper left", bbox_to_anchor=(1, 1)) + except Exception as e: + logging.debug(f"Legend does not exist.") + + plt.tight_layout() + + if show_plot: + plt.show() + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "boxplot_summary.csv") + saved_files = save_outputs({output_file: summary_df}) + + # Also save the figure if specified + figure_file = params.get("Figure_File", None) + if figure_file: + saved_files.update(save_outputs({figure_file: fig})) + + logging.info(f"Boxplot completed → {saved_files[output_file]}") + return saved_files + else: + # Return the figure and summary dataframe for in-memory workflows + logging.info( + "Returning figure and summary dataframe (not saving to file)" + ) + return fig, summary_df + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python boxplot_template.py ", + file=sys.stderr + ) + sys.exit(1) + + # Set up logging for CLI usage + logging.basicConfig(level=logging.INFO) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned figure and summary dataframe") \ No newline at end of file diff --git a/tests/templates/test_boxplot_template.py b/tests/templates/test_boxplot_template.py new file mode 100644 index 00000000..46d57e8c --- /dev/null +++ b/tests/templates/test_boxplot_template.py @@ -0,0 +1,422 @@ +# tests/templates/test_boxplot_template.py +"""Unit tests for the Boxplot template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import matplotlib +matplotlib.use("Agg") # Headless backend for CI + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.boxplot_template import run_from_json + + +def mock_adata_for_boxplot(n_cells: int = 20) -> ad.AnnData: + """Return a minimal synthetic AnnData for boxplot tests.""" + rng = np.random.default_rng(0) + + # Create observations with annotations + obs = pd.DataFrame({ + "cell_type": ["B cells", "T cells"] * (n_cells // 2), + "condition": ["Control", "Treatment"] * (n_cells // 2) + }) + + # Create expression matrix with 3 features + n_features = 3 + x_mat = rng.poisson(lam=5, size=(n_cells, n_features)) + 1.0 + + # Create var dataframe with feature names + var = pd.DataFrame( + index=[f"Gene_{i}" for i in range(n_features)] + ) + + adata = ad.AnnData(X=x_mat, obs=obs, var=var) + + # Add a normalized layer + adata.layers["normalized"] = np.log1p(adata.X) + + return adata + + +class TestBoxplotTemplate(unittest.TestCase): + """Unit tests for the Boxplot template.""" + + def _create_mock_boxplot_return(self, df_data=None): + """Helper to create standard mock returns.""" + mock_fig = MagicMock() + mock_ax = MagicMock() + if df_data is None: + df_data = {'Gene_0': [1]} + mock_df = pd.DataFrame(df_data) + return mock_fig, mock_ax, mock_df + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input_data.pickle" + ) + self.out_file = "boxplot_summary.csv" + + # Save minimal mock data as pickle + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata_for_boxplot(), f) + + # Minimal parameters + self.params = { + "Upstream_Analysis": self.in_file, + "Primary_Annotation": "cell_type", + "Secondary_Annotation": "None", + "Table_to_Visualize": "Original", + "Feature_s_to_Plot": ["All"], + "Value_Axis_Log_Scale": False, + "Figure_Title": "BoxPlot", + "Horizontal_Plot": False, + "Figure_Width": 12, + "Figure_Height": 8, + "Figure_DPI": 300, + "Font_Size": 10, + "Keep_Outliers": True, + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + @patch('spac.templates.boxplot_template.boxplot') + @patch('matplotlib.pyplot.show') + def test_run_with_save(self, mock_show, mock_boxplot) -> None: + """Test boxplot with file saving.""" + # Mock the boxplot function to return figure, ax, and dataframe + mock_fig, mock_ax, mock_df = self._create_mock_boxplot_return({ + 'Gene_0': np.random.RandomState(42).rand(20), + 'Gene_1': np.random.RandomState(42).rand(20), + 'Gene_2': np.random.RandomState(42).rand(20) + }) + mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) + + # Suppress warnings for cleaner test output + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test with save_results=True (default) + saved_files = run_from_json(self.params) + self.assertIn(self.out_file, saved_files) + + # Verify boxplot was called with correct parameters + mock_boxplot.assert_called_once() + call_args = mock_boxplot.call_args + + # Check keyword arguments + self.assertEqual(call_args[1]['annotation'], "cell_type") + self.assertEqual(call_args[1]['second_annotation'], None) + self.assertEqual(call_args[1]['layer'], None) # Original -> None + self.assertEqual(call_args[1]['log_scale'], False) + self.assertEqual(call_args[1]['orient'], "v") + self.assertEqual(call_args[1]['showfliers'], True) + + @patch('spac.templates.boxplot_template.boxplot') + @patch('matplotlib.pyplot.show') + def test_run_without_save(self, mock_show, mock_boxplot) -> None: + """Test boxplot without file saving.""" + # Mock the boxplot function + mock_fig, mock_ax, mock_df = self._create_mock_boxplot_return({ + 'Gene_0': [1, 2, 3], + 'Gene_1': [4, 5, 6], + 'Gene_2': [7, 8, 9] + }) + mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) + + # Test with save_results=False + fig, summary_df = run_from_json( + self.params, save_results=False + ) + + # Verify we got the figure and summary dataframe back + self.assertEqual(fig, mock_fig) + self.assertIsInstance(summary_df, pd.DataFrame) + # Check that summary has statistics + self.assertIn('count', summary_df['index'].values) + self.assertIn('mean', summary_df['index'].values) + self.assertIn('std', summary_df['index'].values) + + @patch('spac.templates.boxplot_template.boxplot') + @patch('matplotlib.pyplot.show') + def test_all_features_plotting(self, mock_show, mock_boxplot) -> None: + """Test plotting all features.""" + mock_fig, mock_ax, mock_df = self._create_mock_boxplot_return({ + 'Gene_0': [1], 'Gene_1': [2] + }) + mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) + + run_from_json(self.params, save_results=False) + + # Verify features parameter - should be all gene names + call_args = mock_boxplot.call_args + features = call_args[1]['features'] + self.assertEqual(len(features), 3) # mock data has 3 genes + self.assertIn('Gene_0', features) + self.assertIn('Gene_1', features) + self.assertIn('Gene_2', features) + + @patch('spac.templates.boxplot_template.boxplot') + @patch('matplotlib.pyplot.show') + def test_specific_features(self, mock_show, mock_boxplot) -> None: + """Test plotting specific features.""" + params_specific = self.params.copy() + params_specific["Feature_s_to_Plot"] = ["Gene_0", "Gene_2"] + + mock_fig = MagicMock() + mock_ax = MagicMock() + mock_df = pd.DataFrame({'Gene_0': [1], 'Gene_2': [2]}) + mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) + + run_from_json(params_specific, save_results=False) + + # Verify specific features were passed + call_args = mock_boxplot.call_args + self.assertEqual( + call_args[1]['features'], ["Gene_0", "Gene_2"] + ) + + @patch('spac.templates.boxplot_template.boxplot') + @patch('matplotlib.pyplot.show') + def test_layer_selection(self, mock_show, mock_boxplot) -> None: + """Test different layer selections.""" + # Test normalized layer + params_norm = self.params.copy() + params_norm["Table_to_Visualize"] = "normalized" + + mock_fig = MagicMock() + mock_ax = MagicMock() + mock_df = pd.DataFrame({'Gene_0': [1]}) + mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) + + run_from_json(params_norm, save_results=False) + + call_args = mock_boxplot.call_args + self.assertEqual(call_args[1]['layer'], "normalized") + + @patch('spac.templates.boxplot_template.boxplot') + @patch('matplotlib.pyplot.show') + def test_horizontal_orientation(self, mock_show, mock_boxplot) -> None: + """Test horizontal plot orientation.""" + params_horiz = self.params.copy() + params_horiz["Horizontal_Plot"] = True + + mock_fig = MagicMock() + mock_ax = MagicMock() + mock_df = pd.DataFrame({'Gene_0': [1]}) + mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) + + run_from_json(params_horiz, save_results=False) + + call_args = mock_boxplot.call_args + self.assertEqual(call_args[1]['orient'], "h") + + @patch('spac.templates.boxplot_template.boxplot') + @patch('matplotlib.pyplot.show') + def test_secondary_annotation(self, mock_show, mock_boxplot) -> None: + """Test with secondary annotation.""" + params_second = self.params.copy() + params_second["Secondary_Annotation"] = "condition" + + mock_fig = MagicMock() + mock_ax = MagicMock() + mock_df = pd.DataFrame({'Gene_0': [1]}) + mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) + + run_from_json(params_second, save_results=False) + + call_args = mock_boxplot.call_args + self.assertEqual(call_args[1]['second_annotation'], "condition") + + @patch('spac.templates.boxplot_template.boxplot') + @patch('matplotlib.pyplot.show') + def test_log_scale(self, mock_show, mock_boxplot) -> None: + """Test log scale option.""" + params_log = self.params.copy() + params_log["Value_Axis_Log_Scale"] = True + + mock_fig = MagicMock() + mock_ax = MagicMock() + mock_df = pd.DataFrame({'Gene_0': [1]}) + mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) + + run_from_json(params_log, save_results=False) + + call_args = mock_boxplot.call_args + self.assertEqual(call_args[1]['log_scale'], True) + + @patch('spac.templates.boxplot_template.boxplot') + @patch('matplotlib.pyplot.show') + def test_outliers_option(self, mock_show, mock_boxplot) -> None: + """Test outliers (showfliers) option.""" + params_no_outliers = self.params.copy() + params_no_outliers["Keep_Outliers"] = False + + mock_fig = MagicMock() + mock_ax = MagicMock() + mock_df = pd.DataFrame({'Gene_0': [1]}) + mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) + + run_from_json(params_no_outliers, save_results=False) + + call_args = mock_boxplot.call_args + self.assertEqual(call_args[1]['showfliers'], False) + + @patch('spac.templates.boxplot_template.boxplot') + @patch('matplotlib.pyplot.show') + @patch('matplotlib.pyplot.subplots') + def test_figure_parameters(self, mock_subplots, mock_show, mock_boxplot) -> None: + """Test figure configuration parameters.""" + params_fig = self.params.copy() + params_fig.update({ + "Figure_Width": 15, + "Figure_Height": 10, + "Figure_DPI": 150, + "Font_Size": 14 + }) + + # Mock the figure and axes from plt.subplots + mock_fig = MagicMock() + mock_ax = MagicMock() + mock_subplots.return_value = (mock_fig, mock_ax) + + # Mock the boxplot return + mock_df = pd.DataFrame({'Gene_0': [1]}) + mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) + + run_from_json(params_fig, save_results=False) + + # Verify figure methods were called with correct values + mock_fig.set_size_inches.assert_called_with(15, 10) + mock_fig.set_dpi.assert_called_with(150) + + @patch('spac.templates.boxplot_template.boxplot') + @patch('matplotlib.pyplot.show') + @patch('spac.templates.boxplot_template.logging.info') + @patch('spac.templates.boxplot_template.logging.debug') + def test_console_output(self, mock_debug, mock_info, mock_show, mock_boxplot) -> None: + """Test that summary statistics are logged to console.""" + mock_fig, mock_ax, mock_df = self._create_mock_boxplot_return({ + 'Gene_0': [1, 2, 3], + 'Gene_1': [4, 5, 6] + }) + mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) + + run_from_json(self.params, save_results=False) + + # Check that appropriate messages were logged + info_calls = [str(call[0][0]) for call in mock_info.call_args_list + if call[0]] + + # Should log "Plotting All Features" + self.assertTrue( + any("Plotting All Features" in msg for msg in info_calls) + ) + + # Should log "Summary statistics of the dataset:" + self.assertTrue( + any("Summary statistics" in msg for msg in info_calls) + ) + + @patch('spac.templates.boxplot_template.boxplot') + @patch('matplotlib.pyplot.show') + def test_json_file_input(self, mock_show, mock_boxplot) -> None: + """Test with JSON file input.""" + mock_fig = MagicMock() + mock_ax = MagicMock() + mock_df = pd.DataFrame({'Gene_0': [1]}) + mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) + + json_path = os.path.join(self.tmp_dir.name, "boxplot_params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result = run_from_json(json_path, save_results=False) + + # Should return tuple when save_results=False + self.assertIsInstance(result, tuple) + self.assertEqual(len(result), 2) + + @patch('spac.templates.boxplot_template.boxplot') + @patch('matplotlib.pyplot.show') + def test_no_annotation(self, mock_show, mock_boxplot) -> None: + """Test with no annotation (None values).""" + params_no_annot = self.params.copy() + params_no_annot["Primary_Annotation"] = "None" + + mock_fig = MagicMock() + mock_ax = MagicMock() + mock_df = pd.DataFrame({'Gene_0': [1]}) + mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) + + run_from_json(params_no_annot, save_results=False) + + call_args = mock_boxplot.call_args + self.assertIsNone(call_args[1]['annotation']) + + @patch('spac.templates.boxplot_template.boxplot') + @patch('matplotlib.pyplot.show') + def test_legend_handling(self, mock_show, mock_boxplot) -> None: + """Test legend positioning handling.""" + mock_fig = MagicMock() + mock_ax = MagicMock() + mock_df = pd.DataFrame({'Gene_0': [1]}) + mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) + + # Test both with and without legend error + with patch('seaborn.move_legend') as mock_move_legend: + # Case 1: Legend exists + run_from_json(self.params, save_results=False) + mock_move_legend.assert_called_once() + + # Case 2: Legend doesn't exist (raises exception) + mock_move_legend.side_effect = Exception("No legend") + run_from_json(self.params, save_results=False) + # Should not raise error, just print message + + def test_parameter_validation(self) -> None: + """Test that missing required parameters raise errors.""" + params_missing = self.params.copy() + del params_missing["Upstream_Analysis"] + + with self.assertRaises(KeyError) as context: + run_from_json(params_missing) + + self.assertIn("Upstream_Analysis", str(context.exception)) + + @patch('spac.templates.boxplot_template.boxplot') + @patch('matplotlib.pyplot.show') + def test_save_figure_option(self, mock_show, mock_boxplot) -> None: + """Test saving figure to file.""" + params_fig_save = self.params.copy() + params_fig_save["Figure_File"] = "boxplot_figure.png" + + mock_fig, mock_ax, mock_df = self._create_mock_boxplot_return() + mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) + + saved_files = run_from_json(params_fig_save) + + # Should save both summary and figure + self.assertIn(self.out_file, saved_files) + self.assertIn("boxplot_figure.png", saved_files) + self.assertEqual(len(saved_files), 2) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From cd5abd005d35678351867ae14020ca9d57317e02 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 28 Jul 2025 22:02:15 -0400 Subject: [PATCH 025/102] fix(boxplot_template): address minor comments from copilot --- src/spac/templates/boxplot_template.py | 2 +- tests/templates/test_boxplot_template.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/spac/templates/boxplot_template.py b/src/spac/templates/boxplot_template.py index 009ce286..6207c8b4 100644 --- a/src/spac/templates/boxplot_template.py +++ b/src/spac/templates/boxplot_template.py @@ -131,7 +131,7 @@ def run_from_json( try: sns.move_legend(ax, "upper left", bbox_to_anchor=(1, 1)) except Exception as e: - logging.debug(f"Legend does not exist.") + logging.debug("Legend does not exist.") plt.tight_layout() diff --git a/tests/templates/test_boxplot_template.py b/tests/templates/test_boxplot_template.py index 46d57e8c..adccf3f5 100644 --- a/tests/templates/test_boxplot_template.py +++ b/tests/templates/test_boxplot_template.py @@ -101,10 +101,11 @@ def tearDown(self) -> None: def test_run_with_save(self, mock_show, mock_boxplot) -> None: """Test boxplot with file saving.""" # Mock the boxplot function to return figure, ax, and dataframe + rng = np.random.default_rng(42) mock_fig, mock_ax, mock_df = self._create_mock_boxplot_return({ - 'Gene_0': np.random.RandomState(42).rand(20), - 'Gene_1': np.random.RandomState(42).rand(20), - 'Gene_2': np.random.RandomState(42).rand(20) + 'Gene_0': rng.random(20), + 'Gene_1': rng.random(20), + 'Gene_2': rng.random(20) }) mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) From 3380427ecbd6d743aaedfb954d612905153079e4 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Wed, 30 Jul 2025 22:57:28 -0400 Subject: [PATCH 026/102] feat(histogram_template): add histogram_template and unit tests --- src/spac/templates/histogram_template.py | 282 +++++++++++++++++++++ tests/templates/test_histogram_template.py | 222 ++++++++++++++++ 2 files changed, 504 insertions(+) create mode 100644 src/spac/templates/histogram_template.py create mode 100644 tests/templates/test_histogram_template.py diff --git a/src/spac/templates/histogram_template.py b/src/spac/templates/histogram_template.py new file mode 100644 index 00000000..997322a3 --- /dev/null +++ b/src/spac/templates/histogram_template.py @@ -0,0 +1,282 @@ +""" +Platform-agnostic Histogram template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.histogram_template import run_from_json +>>> run_from_json("examples/histogram_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, Optional, Tuple +import pandas as pd +import matplotlib.pyplot as plt +import seaborn as sns +import warnings + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.visualization import histogram +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True, + show_plot: bool = False +) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: + """ + Execute Histogram analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the figure and + dataframe directly for in-memory workflows. Default is True. + show_plot : bool, optional + Whether to display the plot. Default is False. + + Returns + ------- + dict or tuple + If save_results=True: Dictionary of saved file paths + If save_results=False: Tuple of (figure, dataframe) + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + feature = text_to_value(params.get("Feature", "None")) + annotation = text_to_value(params.get("Annotation", "None")) + layer = params.get("Table_", "Original") + group_by = params.get("Group_by", "None") + together = params.get("Together", True) + fig_width = params.get("Figure_Width", 8) + fig_height = params.get("Figure_Height", 6) + font_size = params.get("Font_Size", 12) + fig_dpi = params.get("Figure_DPI", 300) + legend_location = params.get("Legend_Location", "best") + legend_in_figure = params.get("Legend_in_Figure", False) + take_X_log = params.get("Take_X_Log", False) + take_Y_log = params.get("Take_Y_log", False) + multiple = params.get("Multiple", "dodge") + shrink = params.get("Shrink_Number", 1) + bins = params.get("Bins", "auto") + alpha = params.get("Bin_Transparency", 0.75) + stat = params.get("Stat", "count") + x_rotate = params.get("X_Axis_Label_Rotation", 0) + histplot_by = params.get("Plot_By", "Annotation") + + # Close all existing figures to prevent extra plots + plt.close('all') + existing_fig_nums = plt.get_fignums() + + plt.rcParams.update({'font.size': font_size}) + + # Adjust feature and annotation based on histplot_by + if histplot_by == "Annotation": + feature = None + else: + annotation = None + + # If both feature and annotation are None, set default + if feature is None and annotation is None: + if histplot_by == "Annotation": + if adata.obs.columns.size > 0: + annotation = adata.obs.columns[0] + print( + f'No annotation specified. Using the first annotation ' + f'"{annotation}" as default.' + ) + else: + raise ValueError( + 'No annotations available in adata.obs to plot.' + ) + else: + if adata.var_names.size > 0: + feature = adata.var_names[0] + print( + f'No feature specified. Using the first feature ' + f'"{feature}" as default.' + ) + else: + raise ValueError( + 'No features available in adata.var_names to plot.' + ) + + # Validate and set bins + if feature is not None: + bins = text_to_value( + bins, + default_none_text="auto", + to_int=True, + param_name="bins" + ) + if bins is None: + num_rows = adata.X.shape[0] + bins = max(int(2 * (num_rows ** (1/3))), 1) + elif bins <= 0: + raise ValueError( + f'Bins should be a positive integer. Received "{bins}"' + ) + elif annotation is not None: + if take_X_log: + take_X_log = False + print( + "Warning: Take X log should only apply to feature. " + "Setting Take X Log to False." + ) + if bins != 'auto': + bins = 'auto' + print( + "Warning: Bin number should only apply to feature. " + "Setting bin number calculation to auto." + ) + + if (x_rotate < 0) or (x_rotate > 360): + raise ValueError( + f'The X label rotation should fall within 0 to 360 degree. ' + f'Received "{x_rotate}".' + ) + + # Initialize the x-variable before the loop + if histplot_by == "Annotation": + x_var = annotation + else: + x_var = feature + + result = histogram( + adata=adata, + feature=feature, + annotation=annotation, + layer=text_to_value(layer, "Original"), + group_by=text_to_value(group_by), + together=together, + ax=None, + x_log_scale=take_X_log, + y_log_scale=take_Y_log, + multiple=multiple, + shrink=shrink, + bins=bins, + alpha=alpha, + stat=stat + ) + + fig = result["fig"] + axs = result["axs"] + df_counts = result["df"] + + # Set figure size and dpi + fig.set_size_inches(fig_width, fig_height) + fig.set_dpi(fig_dpi) + + # Ensure axes is a list + if isinstance(axs, list): + axes = axs + else: + axes = [axs] + + # Close any extra figures created during the histogram call + fig_nums_after = plt.get_fignums() + new_fig_nums = [ + num for num in fig_nums_after if num not in existing_fig_nums + ] + histogram_fig_num = fig.number + + for num in new_fig_nums: + if num != histogram_fig_num: + plt.close(plt.figure(num)) + print(f"Closed extra figure {num}") + + # Process each axis + for ax in axes: + if feature: + print(f'Plotting Feature: "{feature}"') + if ax.get_legend() is not None: + if legend_in_figure: + sns.move_legend(ax, legend_location) + else: + sns.move_legend( + ax, legend_location, bbox_to_anchor=(1, 1) + ) + + # Rotate x labels + ax.tick_params(axis='x', rotation=x_rotate) + + # Set titles based on group_by + if text_to_value(group_by): + if together: + for ax in axes: + ax.set_title( + f'Histogram of "{x_var}" grouped by "{group_by}"' + ) + else: + # compute unique groups directly from adata.obs. + unique_groups = adata.obs[ + text_to_value(group_by) + ].dropna().unique() + if len(axes) != len(unique_groups): + print( + "Warning: Number of axes does not match number of " + "groups. Titles may not correspond correctly." + ) + for ax, grp in zip(axes, unique_groups): + ax.set_title( + f'Histogram of "{x_var}" for group: "{grp}"' + ) + else: + for ax in axes: + ax.set_title(f'Count plot of "{x_var}"') + + plt.tight_layout() + + print("Displaying top 10 rows of histogram dataframe:") + print(df_counts.head(10)) + + if show_plot: + plt.show() + + plt.close('all') + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "plots.csv") + saved_files = save_outputs({output_file: df_counts}) + + print(f"Histogram completed → {saved_files[output_file]}") + return saved_files + else: + # Return the figure and dataframe directly for in-memory workflows + print("Returning figure and dataframe (not saving to file)") + return fig, df_counts + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python histogram_template.py ") + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned figure and dataframe") \ No newline at end of file diff --git a/tests/templates/test_histogram_template.py b/tests/templates/test_histogram_template.py new file mode 100644 index 00000000..ebe8d48e --- /dev/null +++ b/tests/templates/test_histogram_template.py @@ -0,0 +1,222 @@ +# tests/templates/test_histogram_template.py +"""Unit tests for the Histogram template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import matplotlib +matplotlib.use("Agg") # Headless backend for CI + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.histogram_template import run_from_json + + +def mock_adata_with_features(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + + # Simple expression data + x_mat = rng.normal(size=(n_cells, 3)) + + # Simple observations + obs = pd.DataFrame({ + "cell_type": ["TypeA", "TypeB"] * (n_cells // 2) + }) + + # Simple var names + var = pd.DataFrame(index=["Gene1", "Gene2", "Gene3"]) + + return ad.AnnData(X=x_mat, obs=obs, var=var) + + +class TestHistogramTemplate(unittest.TestCase): + """Unit tests for the Histogram template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "plots.csv" + + # Save minimal mock data as pickle + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata_with_features(), f) + + # Minimal parameters + self.params = { + "Upstream_Analysis": self.in_file, + "Plot_By": "Annotation", + "Annotation": "cell_type", + "Feature": "None", + "Table_": "Original", + "Group_by": "None", + "Together": True, + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + @patch('spac.templates.histogram_template.histogram') + @patch('seaborn.move_legend') # Mock seaborn to avoid legend issues + def test_run_with_save(self, mock_move_legend, mock_histogram) -> None: + """Test basic run with file saving.""" + # Mock the histogram function + mock_fig = MagicMock() + mock_fig.number = 1 + mock_fig.set_size_inches = MagicMock() + mock_fig.set_dpi = MagicMock() + + mock_ax = MagicMock() + mock_ax.get_legend.return_value = None + mock_ax.tick_params = MagicMock() + mock_ax.set_title = MagicMock() + + mock_df = pd.DataFrame({ + 'category': ['TypeA', 'TypeB'], + 'count': [5, 5] + }) + + mock_histogram.return_value = { + "fig": mock_fig, + "axs": mock_ax, + "df": mock_df + } + + # Run with save_results=True (default) + saved_files = run_from_json(self.params) + + # Check that file was saved + self.assertIn(self.out_file, saved_files) + self.assertTrue(os.path.exists(saved_files[self.out_file])) + + # Verify histogram was called + mock_histogram.assert_called_once() + + @patch('spac.templates.histogram_template.histogram') + @patch('seaborn.move_legend') + def test_run_without_save(self, mock_move_legend, mock_histogram) -> None: + """Test run without file saving.""" + # Mock the histogram function + mock_fig = MagicMock() + mock_fig.number = 1 + mock_fig.set_size_inches = MagicMock() + mock_fig.set_dpi = MagicMock() + + mock_ax = MagicMock() + mock_ax.get_legend.return_value = None + mock_ax.tick_params = MagicMock() + mock_ax.set_title = MagicMock() + + mock_df = pd.DataFrame({'category': ['A'], 'count': [10]}) + + mock_histogram.return_value = { + "fig": mock_fig, + "axs": mock_ax, + "df": mock_df + } + + # Run with save_results=False + fig, df = run_from_json(self.params, save_results=False) + + # Check that we got figure and dataframe + self.assertEqual(fig, mock_fig) + self.assertIsInstance(df, pd.DataFrame) + + def test_json_file_input(self) -> None: + """Test that JSON file input works.""" + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + with patch('spac.templates.histogram_template.histogram') as mock_hist: + with patch('seaborn.move_legend'): + mock_hist.return_value = { + "fig": MagicMock(number=1), + "axs": MagicMock(), + "df": pd.DataFrame() + } + + result = run_from_json(json_path, save_results=False) + self.assertIsInstance(result, tuple) + + def test_error_messages(self) -> None: + """Test exact error messages for key validations.""" + # Test 1: No annotations available + adata_no_obs = ad.AnnData(X=np.random.rand(5, 3)) + with open(self.in_file, 'wb') as f: + pickle.dump(adata_no_obs, f) + + params_bad = self.params.copy() + params_bad["Annotation"] = "None" + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + expected_msg = 'No annotations available in adata.obs to plot.' + self.assertEqual(str(context.exception), expected_msg) + + # Test 2: Invalid rotation angle + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata_with_features(), f) + + params_bad_rotate = self.params.copy() + params_bad_rotate["X_Axis_Label_Rotation"] = 400 + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad_rotate) + + expected_msg = ( + 'The X label rotation should fall within 0 to 360 degree. ' + 'Received "400".' + ) + self.assertEqual(str(context.exception), expected_msg) + + @patch('spac.templates.histogram_template.histogram') + @patch('seaborn.move_legend') + @patch('builtins.print') + def test_console_output( + self, mock_print, mock_move_legend, mock_histogram + ) -> None: + """Test that dataframe is printed to console.""" + # Mock the histogram function + mock_df = pd.DataFrame({ + 'category': ['TypeA', 'TypeB'], + 'count': [5, 5] + }) + + mock_histogram.return_value = { + "fig": MagicMock(number=1), + "axs": MagicMock(), + "df": mock_df + } + + run_from_json(self.params, save_results=False) + + # Check that dataframe info was printed + print_calls = [str(call[0][0]) for call in mock_print.call_args_list + if call[0]] + + # Should print "Displaying top 10 rows" + self.assertTrue( + any("Displaying top 10 rows" in msg for msg in print_calls) + ) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 51ba1c4609e5d4435e612faf6b632bd8f1f76927 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Wed, 30 Jul 2025 23:19:20 -0400 Subject: [PATCH 027/102] fix(histogram_template): fix odd number of cells in test --- tests/templates/test_histogram_template.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/templates/test_histogram_template.py b/tests/templates/test_histogram_template.py index ebe8d48e..e64603c7 100644 --- a/tests/templates/test_histogram_template.py +++ b/tests/templates/test_histogram_template.py @@ -32,9 +32,10 @@ def mock_adata_with_features(n_cells: int = 10) -> ad.AnnData: # Simple expression data x_mat = rng.normal(size=(n_cells, 3)) - # Simple observations + # Simple observations- fixed to handle odd n_cells obs = pd.DataFrame({ - "cell_type": ["TypeA", "TypeB"] * (n_cells // 2) + "cell_type": (["TypeA", "TypeB"] * (n_cells // 2) + + ["TypeA"] * (n_cells % 2))[:n_cells] }) # Simple var names From 0f26c08744f5611f0aa92a9a60b0ad20815e8d6e Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 00:36:37 -0400 Subject: [PATCH 028/102] feat(spatial_plot_temp): add spatial_plot_template.py and unit tests --- src/spac/templates/spatial_plot_template.py | 221 ++++++++++++++ tests/templates/test_spatial_plot_template.py | 270 ++++++++++++++++++ 2 files changed, 491 insertions(+) create mode 100644 src/spac/templates/spatial_plot_template.py create mode 100644 tests/templates/test_spatial_plot_template.py diff --git a/src/spac/templates/spatial_plot_template.py b/src/spac/templates/spatial_plot_template.py new file mode 100644 index 00000000..f91df425 --- /dev/null +++ b/src/spac/templates/spatial_plot_template.py @@ -0,0 +1,221 @@ +""" +Platform-agnostic Spatial Plot template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.spatial_plot_template import run_from_json +>>> run_from_json("examples/spatial_plot_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, List, Optional +import matplotlib.pyplot as plt +from functools import partial + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.visualization import spatial_plot +from spac.data_utils import select_values +from spac.utils import check_annotation +from spac.templates.template_utils import ( + load_input, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True, + show_plots: bool = True +) -> Union[Dict[str, str], None]: + """ + Execute Spatial Plot analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns None. + Default is True. + show_plots : bool, optional + Whether to display the plots. Default is True. + + Returns + ------- + dict or None + If save_results=True: Dictionary of saved file paths + If save_results=False: None + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters exactly as in NIDAP template + annotation = params["Annotation_to_Highlight"] + feature = params["Feature_to_Highlight"] + layer = params["Table"] + + alpha = params["Dot_Transparency"] + spot_size = params["Dot_Size"] + image_height = params["Figure_Height"] + image_width = params["Figure_Width"] + dpi = params["Figure_DPI"] + font_size = params["Font_Size"] + vmin = params["Lower_Colorbar_Bound"] + vmax = params["Upper_Colorbar_Bound"] + color_by = params["Color_By"] + stratify = params["Stratify"] + stratify_by = params["Stratify_By"] + + ##--------------- ## + ## Error Messages ## + ## -------------- ## + if stratify and len(stratify_by) == 0: + raise ValueError( + 'Please set at least one annotation in the "Stratify By" ' + 'option, or set the "Stratify" to False.' + ) + check_annotation( + adata, + annotations=stratify_by + ) + + # Process feature and annotation with text_to_value + feature = text_to_value(feature) + annotation = text_to_value(annotation) + + if color_by == "Annotation": + feature = None + else: + annotation = None + + ## --------- ## + ## Functions ## + ## --------- ## + + ## --------------- ## + ## Main Code Block ## + ## --------------- ## + layer = text_to_value(layer, "Original") + + prefilled_spatial = partial( + spatial_plot, + spot_size=spot_size, + alpha=alpha, + vmin=vmin, + vmax=vmax, + annotation=annotation, + feature=feature, + layer=layer + ) + + # Track figures for optional saving + figures = [] + + if not stratify: + plt.rcParams['font.size'] = font_size + fig, ax = plt.subplots( + figsize=(image_width, image_height), dpi=dpi + ) + + ax = prefilled_spatial(adata=adata, ax=ax) + + if color_by == "Annotation": + title = f'Annotation: {annotation}' + else: + title = f'Table:"{layer}" \n Feature:"{feature}"' + ax[0].set_title(title) + + figures.append(fig) + + if show_plots: + plt.show() + else: + combined_label = "concatenated_label" + + adata.obs[combined_label] = adata.obs[stratify_by].astype(str).agg( + '_'.join, axis=1 + ) + + unique_values = adata.obs[combined_label].unique() + + print(unique_values) + + max_length = min(len(unique_values), 20) + if len(unique_values) > 20: + print( + f'WARNING: There are "{len(unique_values)}" unique plots, ' + 'displaying only the first 20 plots.' + ) + + for value in unique_values[:max_length]: + filtered_adata = select_values( + data=adata, annotation=combined_label, values=value + ) + + fig, ax = plt.subplots( + figsize=(image_width, image_height), dpi=dpi + ) + + ax = prefilled_spatial(adata=filtered_adata, ax=ax) + + if color_by == "Annotation": + title = f'Annotation: {annotation}' + else: + title = f'Table:"{layer}" \n Feature:"{feature}"' + title = f'{title}\n Stratify by: {value}' + ax[0].set_title(title) + + figures.append(fig) + + if show_plots: + plt.show() + + # Handle saving if requested (separate from NIDAP logic) + if save_results and figures: + saved_files = {} + output_prefix = params.get("Output_File", "spatial_plot") + + if len(figures) == 1: + output_file = f"{output_prefix}.png" + figures[0].savefig(output_file, dpi=dpi, bbox_inches='tight') + saved_files[output_file] = output_file + else: + for i, fig in enumerate(figures): + output_file = f"{output_prefix}_plot_{i+1}.png" + fig.savefig(output_file, dpi=dpi, bbox_inches='tight') + saved_files[output_file] = output_file + + # Close figures after saving + for fig in figures: + plt.close(fig) + + print(f"Spatial Plot completed → {list(saved_files.keys())}") + return saved_files + + return None + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python spatial_plot_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if result: + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") \ No newline at end of file diff --git a/tests/templates/test_spatial_plot_template.py b/tests/templates/test_spatial_plot_template.py new file mode 100644 index 00000000..72d6c2b8 --- /dev/null +++ b/tests/templates/test_spatial_plot_template.py @@ -0,0 +1,270 @@ +# tests/templates/test_spatial_plot_template.py +"""Unit tests for the Spatial Plot template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock, call + +import matplotlib +matplotlib.use("Agg") # Headless backend for CI + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.spatial_plot_template import run_from_json + + +def mock_adata_spatial(n_cells: int = 100) -> ad.AnnData: + """Return a minimal synthetic AnnData with spatial coordinates.""" + rng = np.random.default_rng(0) + + # Create expression matrix + n_features = 5 + X = rng.normal(size=(n_cells, n_features)) + + # Create annotations + obs = pd.DataFrame({ + "cell_type": rng.choice(["TypeA", "TypeB", "TypeC"], n_cells), + "region": rng.choice(["Region1", "Region2"], n_cells), + "slide": rng.choice(["Slide1", "Slide2"], n_cells) + }) + + # Create spatial coordinates + spatial_coords = rng.random((n_cells, 2)) * 1000 + + # Create AnnData object + adata = ad.AnnData(X=X, obs=obs) + adata.obsm["spatial"] = spatial_coords + + # Add feature names + adata.var_names = [f"Feature_{i}" for i in range(n_features)] + + return adata + + +class TestSpatialPlotTemplate(unittest.TestCase): + """Unit tests for the Spatial Plot template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "spatial_data.pickle" + ) + self.out_file = "spatial_plot" + + # Save minimal mock data with spatial info + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata_spatial(), f) + + # Minimal parameters + self.params = { + "Upstream_Analysis": self.in_file, + "Annotation_to_Highlight": "cell_type", + "Feature_to_Highlight": "", + "Table": "Original", + "Dot_Transparency": 0.5, + "Dot_Size": 25, + "Figure_Height": 6, + "Figure_Width": 12, + "Figure_DPI": 200, + "Font_Size": 12, + "Lower_Colorbar_Bound": 999, + "Upper_Colorbar_Bound": -999, + "Color_By": "Annotation", + "Stratify": False, + "Stratify_By": [], + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + @patch('spac.templates.spatial_plot_template.spatial_plot') + @patch('matplotlib.pyplot.show') + def test_run_single_plot_annotation(self, mock_show, mock_spatial) -> None: + """Test single plot with annotation coloring.""" + # Mock the spatial_plot function to return axes + mock_ax = MagicMock() + mock_spatial.return_value = [mock_ax] + + # Test with annotation coloring + saved_files = run_from_json(self.params) + + # Verify spatial_plot was called correctly + mock_spatial.assert_called_once() + call_kwargs = mock_spatial.call_args[1] + self.assertEqual(call_kwargs['annotation'], 'cell_type') + self.assertIsNone(call_kwargs['feature']) + # layer should be None because text_to_value(layer, "Original") + # converts "Original" to None + self.assertIsNone(call_kwargs['layer']) + self.assertEqual(call_kwargs['spot_size'], 25) + self.assertEqual(call_kwargs['alpha'], 0.5) + + # Check saved files + self.assertIn(f"{self.out_file}.png", saved_files) + + # Verify show was called + mock_show.assert_called_once() + + @patch('spac.templates.spatial_plot_template.spatial_plot') + @patch('matplotlib.pyplot.show') + def test_run_single_plot_feature(self, mock_show, mock_spatial) -> None: + """Test single plot with feature coloring.""" + # Mock the spatial_plot function + mock_ax = MagicMock() + mock_spatial.return_value = [mock_ax] + + # Update params for feature coloring + params_feature = self.params.copy() + params_feature["Color_By"] = "Feature" + params_feature["Feature_to_Highlight"] = "Feature_0" + + result = run_from_json(params_feature) + + # Verify spatial_plot was called with feature + call_kwargs = mock_spatial.call_args[1] + self.assertIsNone(call_kwargs['annotation']) + self.assertEqual(call_kwargs['feature'], 'Feature_0') + + @patch('spac.templates.spatial_plot_template.spatial_plot') + @patch('spac.templates.spatial_plot_template.select_values') + @patch('matplotlib.pyplot.show') + @patch('builtins.print') + def test_run_stratified_plots( + self, mock_print, mock_show, mock_select, mock_spatial + ) -> None: + """Test stratified plots generation.""" + # Mock functions + mock_ax = MagicMock() + mock_spatial.return_value = [mock_ax] + + # Mock select_values to return filtered data + mock_adata = mock_adata_spatial(50) + mock_select.return_value = mock_adata + + # Update params for stratification + params_strat = self.params.copy() + params_strat["Stratify"] = True + params_strat["Stratify_By"] = ["region", "slide"] + + saved_files = run_from_json(params_strat) + + # Should save multiple files + self.assertIsInstance(saved_files, dict) + self.assertTrue(len(saved_files) > 1) + + # Verify unique values were printed + print_calls = [str(call[0][0]) for call in mock_print.call_args_list] + # Should print unique values array + self.assertTrue(any('Region' in str(call) for call in print_calls)) + + def test_stratify_validation_error(self) -> None: + """Test error when stratify is True but no stratify_by provided.""" + params_bad = self.params.copy() + params_bad["Stratify"] = True + params_bad["Stratify_By"] = [] + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + expected_msg = ( + 'Please set at least one annotation in the "Stratify By" ' + 'option, or set the "Stratify" to False.' + ) + self.assertEqual(str(context.exception), expected_msg) + + @patch('spac.templates.spatial_plot_template.spatial_plot') + @patch('matplotlib.pyplot.show') + def test_run_without_save(self, mock_show, mock_spatial) -> None: + """Test running without saving files.""" + # Mock the spatial_plot function + mock_ax = MagicMock() + mock_spatial.return_value = [mock_ax] + + # Run with save_results=False + result = run_from_json(self.params, save_results=False) + + # Should return None when not saving + self.assertIsNone(result) + + @patch('spac.templates.spatial_plot_template.spatial_plot') + @patch('matplotlib.pyplot.show') + def test_max_plots_warning(self, mock_show, mock_spatial) -> None: + """Test warning when too many unique stratification values.""" + # Create adata with many unique combinations + adata = mock_adata_spatial(100) + # Add a column with many unique values + adata.obs['many_values'] = [f'Val_{i}' for i in range(100)] + + with open(self.in_file, 'wb') as f: + pickle.dump(adata, f) + + # Mock functions + mock_ax = MagicMock() + mock_spatial.return_value = [mock_ax] + + params_many = self.params.copy() + params_many["Stratify"] = True + params_many["Stratify_By"] = ["many_values"] + + with patch('builtins.print') as mock_print: + saved_files = run_from_json(params_many) + + # Check warning was printed + print_calls = [ + str(call[0][0]) for call in mock_print.call_args_list + ] + warning_printed = any( + 'displaying only the first 20 plots' in call + for call in print_calls + ) + self.assertTrue(warning_printed) + + def test_json_file_input(self) -> None: + """Test with JSON file input.""" + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + with patch('spac.templates.spatial_plot_template.spatial_plot'): + with patch('matplotlib.pyplot.show'): + result = run_from_json(json_path, save_results=False) + + # Should return None when save_results=False + self.assertIsNone(result) + + @patch('spac.templates.spatial_plot_template.spatial_plot') + def test_text_to_value_processing(self, mock_spatial) -> None: + """Test that text_to_value is applied to string parameters.""" + # Mock spatial_plot + mock_ax = MagicMock() + mock_spatial.return_value = [mock_ax] + + # Test with "None" strings + params_none = self.params.copy() + params_none["Annotation_to_Highlight"] = "None" + params_none["Color_By"] = "Feature" + params_none["Feature_to_Highlight"] = "Feature_0" + + with patch('matplotlib.pyplot.show'): + run_from_json(params_none, save_results=False) + + # Annotation should be None after text_to_value + call_kwargs = mock_spatial.call_args[1] + self.assertIsNone(call_kwargs['annotation']) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 028a049bf5857cdb51eff4a76a4940dc50100872 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 01:13:56 -0400 Subject: [PATCH 029/102] fix(spatial_plot_temp): addrss copilot comments spatial_plot_template.py --- src/spac/templates/spatial_plot_template.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/spac/templates/spatial_plot_template.py b/src/spac/templates/spatial_plot_template.py index f91df425..28cfb168 100644 --- a/src/spac/templates/spatial_plot_template.py +++ b/src/spac/templates/spatial_plot_template.py @@ -75,9 +75,6 @@ def run_from_json( stratify = params["Stratify"] stratify_by = params["Stratify_By"] - ##--------------- ## - ## Error Messages ## - ## -------------- ## if stratify and len(stratify_by) == 0: raise ValueError( 'Please set at least one annotation in the "Stratify By" ' @@ -97,13 +94,6 @@ def run_from_json( else: annotation = None - ## --------- ## - ## Functions ## - ## --------- ## - - ## --------------- ## - ## Main Code Block ## - ## --------------- ## layer = text_to_value(layer, "Original") prefilled_spatial = partial( From ff6cce42b602642a4bd2f211d1dbd1fe6c6fd65e Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 07:56:33 -0400 Subject: [PATCH 030/102] feat(arcsinh_normalization_template): add arcsinh_normalization_template function and unit tests --- .../arcsinh_normalization_template.py | 141 +++++++++++++++ .../test_arcsinh_normalization_template.py | 167 ++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 src/spac/templates/arcsinh_normalization_template.py create mode 100644 tests/templates/test_arcsinh_normalization_template.py diff --git a/src/spac/templates/arcsinh_normalization_template.py b/src/spac/templates/arcsinh_normalization_template.py new file mode 100644 index 00000000..7a7418f6 --- /dev/null +++ b/src/spac/templates/arcsinh_normalization_template.py @@ -0,0 +1,141 @@ +""" +Platform-agnostic Arcsinh Normalization template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.arcsinh_normalization_template import run_from_json +>>> run_from_json("examples/arcsinh_normalization_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union +import pandas as pd + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.transformations import arcsinh_transformation +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], Any]: + """ + Execute Arcsinh Normalization analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the adata object + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or AnnData + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed AnnData object + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + input_layer = params.get("Table_to_Process", "Original") + co_factor = params.get("Co_Factor", "5.0") + percentile = params.get("Percentile", "None") + output_layer = params.get("Output_Table_Name", "arcsinh") + per_batch = params.get("Per_Batch", "False") + annotation = params.get("Annotation", "None") + + input_layer = text_to_value( + input_layer, + default_none_text="Original" + ) + + co_factor = text_to_value( + co_factor, + default_none_text="None", + to_float=True, + param_name="co_factor" + ) + + percentile = text_to_value( + percentile, + default_none_text="None", + to_float=True, + param_name="percentile" + ) + + if per_batch == "True": + per_batch = True + else: + per_batch = False + + annotation = text_to_value( + annotation, + default_none_text="None" + ) + + transformed_data = arcsinh_transformation( + adata, + input_layer=input_layer, + co_factor=co_factor, + percentile=percentile, + output_layer=output_layer, + per_batch=per_batch, + annotation=annotation + ) + + print(f"Transformed data stored in layer: {output_layer}") + dataframe = pd.DataFrame(transformed_data.layers[output_layer]) + print(dataframe.describe()) + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "transform_output.pickle") + # Default to pickle format if no recognized extension + if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): + output_file = output_file + '.pickle' + + saved_files = save_outputs({output_file: transformed_data}) + + print(f"Arcsinh Normalization completed → {saved_files[output_file]}") + return saved_files + else: + # Return the adata object directly for in-memory workflows + print("Returning AnnData object (not saving to file)") + return transformed_data + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python arcsinh_normalization_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned AnnData object") \ No newline at end of file diff --git a/tests/templates/test_arcsinh_normalization_template.py b/tests/templates/test_arcsinh_normalization_template.py new file mode 100644 index 00000000..5738e40c --- /dev/null +++ b/tests/templates/test_arcsinh_normalization_template.py @@ -0,0 +1,167 @@ +# tests/templates/test_arcsinh_normalization_template.py +"""Unit tests for the Arcsinh Normalization template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.arcsinh_normalization_template import run_from_json + + +def mock_adata(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + # Create expression data with some high values to normalize + x_mat = rng.exponential(scale=10, size=(n_cells, 5)) + obs = pd.DataFrame({ + "cell_type": ["TypeA", "TypeB"] * (n_cells // 2), + "batch": ["Batch1", "Batch2"] * (n_cells // 2) + }) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = [f"Marker{i}" for i in range(5)] + # Add an empty layers dict + adata.layers = {} + return adata + + +class TestArcsinhNormalizationTemplate(unittest.TestCase): + """Unit tests for the Arcsinh Normalization template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "output" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters from NIDAP template + self.params = { + "Upstream_Analysis": self.in_file, + "Table_to_Process": "Original", + "Co_Factor": "5.0", + "Percentile": "None", + "Output_Table_Name": "arcsinh", + "Per_Batch": "False", + "Annotation": "None", + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + @patch('spac.templates.arcsinh_normalization_template.' + 'arcsinh_transformation') + def test_complete_io_workflow(self, mock_arcsinh) -> None: + """Single I/O test covering input/output scenarios.""" + # Mock the arcsinh_transformation function + def mock_transform(adata, **kwargs): + # Simulate transformation by adding a layer + adata.layers[kwargs['output_layer']] = ( + np.log1p(adata.X) / 5.0 + ) + return adata + + mock_arcsinh.side_effect = mock_transform + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + # Verify file was saved + self.assertTrue(len(result) > 0) + pickle_files = [ + f for f in result.values() if '.pickle' in str(f) + ] + self.assertTrue(len(pickle_files) > 0) + + # Test 2: Run without saving + result_no_save = run_from_json( + self.params, save_results=False + ) + # Check appropriate return type based on template + self.assertIsInstance(result_no_save, ad.AnnData) + self.assertIn("arcsinh", result_no_save.layers) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + # Verify arcsinh_transformation was called with correct params + call_args = mock_arcsinh.call_args + # "Original" → None + self.assertEqual(call_args[1]['input_layer'], None) + self.assertEqual(call_args[1]['co_factor'], 5.0) + self.assertEqual(call_args[1]['percentile'], None) + self.assertEqual(call_args[1]['output_layer'], "arcsinh") + self.assertEqual(call_args[1]['per_batch'], False) + self.assertEqual(call_args[1]['annotation'], None) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test invalid float conversion for co_factor + params_bad = self.params.copy() + params_bad["Co_Factor"] = "invalid_number" + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + # Check exact error message + expected_msg = ( + "Error: can't convert co_factor to float. " + "Received:\"invalid_number\"" + ) + self.assertEqual(str(context.exception), expected_msg) + + @patch('spac.templates.arcsinh_normalization_template.' + 'arcsinh_transformation') + def test_function_calls(self, mock_arcsinh) -> None: + """Test that main function is called with correct parameters.""" + # Mock the main function to return transformed data + mock_arcsinh.return_value = mock_adata() + mock_arcsinh.return_value.layers["arcsinh"] = np.zeros((10, 5)) + + # Test with percentile instead of co_factor + params_alt = self.params.copy() + params_alt["Co_Factor"] = "None" + params_alt["Percentile"] = "10" + params_alt["Per_Batch"] = "True" + params_alt["Annotation"] = "batch" + + run_from_json(params_alt, save_results=False) + + # Verify function was called correctly + mock_arcsinh.assert_called_once() + call_args = mock_arcsinh.call_args + + # Check specific parameter conversions + self.assertEqual(call_args[1]['co_factor'], None) + self.assertEqual(call_args[1]['percentile'], 10.0) + self.assertEqual(call_args[1]['per_batch'], True) + self.assertEqual(call_args[1]['annotation'], "batch") + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From afca7ff1eb153e3545a805a04be2df4acd283d15 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 08:33:00 -0400 Subject: [PATCH 031/102] fix(test_arcsinh_normalization_template): Handle odd numbers with better list slicing --- .../test_arcsinh_normalization_template.py | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/tests/templates/test_arcsinh_normalization_template.py b/tests/templates/test_arcsinh_normalization_template.py index 5738e40c..46b7dda2 100644 --- a/tests/templates/test_arcsinh_normalization_template.py +++ b/tests/templates/test_arcsinh_normalization_template.py @@ -27,8 +27,8 @@ def mock_adata(n_cells: int = 10) -> ad.AnnData: # Create expression data with some high values to normalize x_mat = rng.exponential(scale=10, size=(n_cells, 5)) obs = pd.DataFrame({ - "cell_type": ["TypeA", "TypeB"] * (n_cells // 2), - "batch": ["Batch1", "Batch2"] * (n_cells // 2) + "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], + "batch": (["Batch1", "Batch2"] * ((n_cells + 1) // 2))[:n_cells] }) adata = ad.AnnData(X=x_mat, obs=obs) adata.var_names = [f"Marker{i}" for i in range(5)] @@ -66,16 +66,13 @@ def setUp(self) -> None: def tearDown(self) -> None: self.tmp_dir.cleanup() - @patch('spac.templates.arcsinh_normalization_template.' - 'arcsinh_transformation') + @patch('spac.templates.arcsinh_normalization_template.arcsinh_transformation') def test_complete_io_workflow(self, mock_arcsinh) -> None: """Single I/O test covering input/output scenarios.""" # Mock the arcsinh_transformation function def mock_transform(adata, **kwargs): # Simulate transformation by adding a layer - adata.layers[kwargs['output_layer']] = ( - np.log1p(adata.X) / 5.0 - ) + adata.layers[kwargs['output_layer']] = np.log1p(adata.X) / 5.0 return adata mock_arcsinh.side_effect = mock_transform @@ -88,15 +85,11 @@ def mock_transform(adata, **kwargs): self.assertIsInstance(result, dict) # Verify file was saved self.assertTrue(len(result) > 0) - pickle_files = [ - f for f in result.values() if '.pickle' in str(f) - ] + pickle_files = [f for f in result.values() if '.pickle' in str(f)] self.assertTrue(len(pickle_files) > 0) # Test 2: Run without saving - result_no_save = run_from_json( - self.params, save_results=False - ) + result_no_save = run_from_json(self.params, save_results=False) # Check appropriate return type based on template self.assertIsInstance(result_no_save, ad.AnnData) self.assertIn("arcsinh", result_no_save.layers) @@ -109,10 +102,9 @@ def mock_transform(adata, **kwargs): result_json = run_from_json(json_path) self.assertIsInstance(result_json, dict) - # Verify arcsinh_transformation was called with correct params + # Verify arcsinh_transformation was called with correct parameters call_args = mock_arcsinh.call_args - # "Original" → None - self.assertEqual(call_args[1]['input_layer'], None) + self.assertEqual(call_args[1]['input_layer'], None) # "Original" → None self.assertEqual(call_args[1]['co_factor'], 5.0) self.assertEqual(call_args[1]['percentile'], None) self.assertEqual(call_args[1]['output_layer'], "arcsinh") @@ -135,8 +127,7 @@ def test_error_validation(self) -> None: ) self.assertEqual(str(context.exception), expected_msg) - @patch('spac.templates.arcsinh_normalization_template.' - 'arcsinh_transformation') + @patch('spac.templates.arcsinh_normalization_template.arcsinh_transformation') def test_function_calls(self, mock_arcsinh) -> None: """Test that main function is called with correct parameters.""" # Mock the main function to return transformed data From b2d68c5fb6cd1dfba38684d855f38aea98d56296 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 09:07:34 -0400 Subject: [PATCH 032/102] feat(zscore_normalization_template): add zscore_normalization_template and unit tests --- .../zscore_normalization_template.py | 109 +++++++++ .../test_zscore_normalization_template.py | 211 ++++++++++++++++++ 2 files changed, 320 insertions(+) create mode 100644 src/spac/templates/zscore_normalization_template.py create mode 100644 tests/templates/test_zscore_normalization_template.py diff --git a/src/spac/templates/zscore_normalization_template.py b/src/spac/templates/zscore_normalization_template.py new file mode 100644 index 00000000..0c8b17da --- /dev/null +++ b/src/spac/templates/zscore_normalization_template.py @@ -0,0 +1,109 @@ +""" +Platform-agnostic Z-Score Normalization template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.zscore_normalization_template import run_from_json +>>> run_from_json("examples/zscore_normalization_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union +import pandas as pd +import pickle + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.transformations import z_score_normalization +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], Any]: + """ + Execute Z-Score Normalization analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the adata object + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or AnnData + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed AnnData object + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + input_layer = params["Table_to_Process"] + output_layer = params["Output_Table_Name"] + + if input_layer == "Original": + input_layer = None + + z_score_normalization( + adata, + output_layer=output_layer, + input_layer=input_layer + ) + + # Convert the normalized layer to a DataFrame and print its summary + post_dataframe = adata.to_df(layer=output_layer) + print(post_dataframe.describe()) + + print(adata) + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "transform_output.pickle") + # Default to pickle format if no recognized extension + if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): + output_file = output_file + '.pickle' + + saved_files = save_outputs({output_file: adata}) + + print(f"Z-Score Normalization completed → {saved_files[output_file]}") + return saved_files + else: + # Return the adata object directly for in-memory workflows + print("Returning AnnData object (not saving to file)") + return adata + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python zscore_normalization_template.py ", + file=sys.stderr + ) + sys.exit(1) + + saved_files = run_from_json(sys.argv[1]) + + if isinstance(saved_files, dict): + print("\nOutput files:") + for filename, filepath in saved_files.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned AnnData object") \ No newline at end of file diff --git a/tests/templates/test_zscore_normalization_template.py b/tests/templates/test_zscore_normalization_template.py new file mode 100644 index 00000000..27099de4 --- /dev/null +++ b/tests/templates/test_zscore_normalization_template.py @@ -0,0 +1,211 @@ +# tests/templates/test_zscore_normalization_template.py +"""Unit tests for the Z-Score Normalization template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.zscore_normalization_template import run_from_json + + +def mock_adata(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], + "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] + }) + x_mat = rng.normal(size=(n_cells, 3)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = ["Marker1", "Marker2", "Marker3"] + + # Add a test layer with different values + adata.layers["test_layer"] = rng.normal(loc=5, scale=2, size=(n_cells, 3)) + + return adata + + +class TestZScoreNormalizationTemplate(unittest.TestCase): + """Unit tests for the Z-Score Normalization template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "normalized_output" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters from JSON template + self.params = { + "Upstream_Analysis": self.in_file, + "Table_to_Process": "Original", + "Output_Table_Name": "z_scores", + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + @patch('spac.templates.zscore_normalization_template.' + 'z_score_normalization') + def test_complete_io_workflow(self, mock_z_score_norm) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Mock the z_score_normalization function + def mock_z_score_side_effect(adata, **kwargs): + # Simulate what z_score_normalization does + output_layer = kwargs.get('output_layer', 'z_scores') + input_layer = kwargs.get('input_layer', None) + + # Create normalized data + if input_layer is None: + data = adata.X + else: + data = adata.layers[input_layer] + + # Simple z-score normalization simulation + normalized = (data - np.mean(data, axis=0)) / np.std(data, axis=0) + adata.layers[output_layer] = normalized + return None + + mock_z_score_norm.side_effect = mock_z_score_side_effect + + # Test 1: Run with default parameters (Original layer) + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + + # Verify z_score_normalization was called correctly + mock_z_score_norm.assert_called_once() + call_args = mock_z_score_norm.call_args + self.assertEqual(call_args[1]['output_layer'], "z_scores") + # Original -> None + self.assertEqual(call_args[1]['input_layer'], None) + + # Test 2: Run without saving + mock_z_score_norm.reset_mock() + result_no_save = run_from_json(self.params, save_results=False) + self.assertIsInstance(result_no_save, ad.AnnData) + self.assertIn("z_scores", result_no_save.layers) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + mock_z_score_norm.reset_mock() + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + # Test 4: Using a specific layer + params_with_layer = self.params.copy() + params_with_layer["Table_to_Process"] = "test_layer" + params_with_layer["Output_Table_Name"] = "test_z_scores" + + mock_z_score_norm.reset_mock() + result_layer = run_from_json(params_with_layer, save_results=False) + + # Verify correct layer was used + call_args = mock_z_score_norm.call_args + self.assertEqual(call_args[1]['input_layer'], "test_layer") + self.assertEqual(call_args[1]['output_layer'], "test_z_scores") + + @patch('builtins.print') + @patch('spac.templates.zscore_normalization_template.' + 'z_score_normalization') + def test_console_output( + self, mock_z_score_norm, mock_print + ) -> None: + """Test that summary statistics are printed to console.""" + # Mock z_score_normalization to create the expected layer + def mock_z_score_side_effect(adata, **kwargs): + output_layer = kwargs.get('output_layer', 'z_scores') + # Create dummy normalized data + adata.layers[output_layer] = np.zeros_like(adata.X) + return None + + mock_z_score_norm.side_effect = mock_z_score_side_effect + + run_from_json(self.params, save_results=False) + + # Check that describe() output was printed + print_calls = [ + str(call[0][0]) for call in mock_print.call_args_list + if call[0] + ] + + # Should contain statistical summary + summary_printed = any( + 'mean' in str(call).lower() or + 'std' in str(call).lower() or + 'count' in str(call).lower() + for call in print_calls + ) + self.assertTrue( + summary_printed, + "DataFrame summary statistics were not printed" + ) + + # Should print adata info + adata_printed = any( + 'AnnData' in str(call) for call in print_calls + ) + self.assertTrue(adata_printed, "AnnData info was not printed") + + def test_missing_upstream_analysis_error(self) -> None: + """Test exact error for missing Upstream_Analysis parameter.""" + params_bad = self.params.copy() + del params_bad["Upstream_Analysis"] + + with self.assertRaises(KeyError) as context: + run_from_json(params_bad) + + self.assertIn("Upstream_Analysis", str(context.exception)) + + @patch('spac.templates.zscore_normalization_template.' + 'z_score_normalization') + def test_output_file_extension_handling( + self, mock_z_score_norm + ) -> None: + """Test that output defaults to pickle format.""" + # Mock z_score_normalization to create the expected layer + def mock_z_score_side_effect(adata, **kwargs): + output_layer = kwargs.get('output_layer', 'z_scores') + # Create dummy normalized data + adata.layers[output_layer] = np.zeros_like(adata.X) + return None + + mock_z_score_norm.side_effect = mock_z_score_side_effect + + params = self.params.copy() + params["Output_File"] = "results.dat" # No standard extension + + saved_files = run_from_json(params) + + # Should save as pickle by default + pickle_files = [f for f in saved_files.values() + if '.pickle' in str(f)] + self.assertTrue(len(pickle_files) > 0) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 06d8feb4d72d132efb6c4db04f6254a8bd69ca04 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 10:11:29 -0400 Subject: [PATCH 033/102] feat(summarize_dataframe_template): add summarize_dataframe_template function and unit tests --- .../templates/summarize_dataframe_template.py | 119 ++++++++ .../test_summarize_dataframe_template.py | 253 ++++++++++++++++++ 2 files changed, 372 insertions(+) create mode 100644 src/spac/templates/summarize_dataframe_template.py create mode 100644 tests/templates/test_summarize_dataframe_template.py diff --git a/src/spac/templates/summarize_dataframe_template.py b/src/spac/templates/summarize_dataframe_template.py new file mode 100644 index 00000000..d3a51410 --- /dev/null +++ b/src/spac/templates/summarize_dataframe_template.py @@ -0,0 +1,119 @@ +""" +Platform-agnostic Summarize DataFrame template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.summarize_dataframe_template import run_from_json +>>> run_from_json("examples/summarize_dataframe_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, List, Optional, Tuple +import pandas as pd +import matplotlib.pyplot as plt + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.data_utils import summarize_dataframe +from spac.visualization import present_summary_as_html +from spac.visualization import present_summary_as_figure +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True, + show_plot: bool = True +) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: + """ + Execute Summarize DataFrame analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the figure and + dataframe directly for in-memory workflows. Default is True. + show_plot : bool, optional + Whether to display the plot. Default is True. + + Returns + ------- + dict or tuple + If save_results=True: Dictionary of saved file paths + If save_results=False: Tuple of (figure, dataframe) + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + # Note: load_input is for AnnData objects; DataFrames from CSV are handled separately + input_path = params["Calculate_Centroids"] + if isinstance(input_path, str) and input_path.endswith('.csv'): + df = pd.read_csv(input_path) + else: + # For pickle files that contain DataFrames + df = load_input(input_path) + + # Extract parameters + columns = params["Columns"] + print_missing_location = params.get("Print_Missing_Location", False) + + # Run the analysis exactly as in NIDAP template + summary = summarize_dataframe( + df, + columns=columns, + print_nan_locations=print_missing_location) + # Generate HTML from the summary. + + fig = present_summary_as_figure(summary) + + if show_plot: + fig.show() # Opens in an interactive Plotly window + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "summary.html") + + # Since the figure is a Plotly figure, we save it as HTML + if not output_file.endswith('.html'): + output_file = output_file + '.html' + + fig.write_html(output_file) + saved_files = {output_file: output_file} + + print(f"Summarize DataFrame completed → {saved_files[output_file]}") + return saved_files + else: + # Return the figure and summary dataframe directly for in-memory workflows + print("Returning figure and dataframe (not saving to file)") + return fig, summary + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python summarize_dataframe_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned figure and dataframe") \ No newline at end of file diff --git a/tests/templates/test_summarize_dataframe_template.py b/tests/templates/test_summarize_dataframe_template.py new file mode 100644 index 00000000..c179eaa0 --- /dev/null +++ b/tests/templates/test_summarize_dataframe_template.py @@ -0,0 +1,253 @@ +# tests/templates/test_summarize_dataframe_template.py +"""Unit tests for the Summarize DataFrame template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.summarize_dataframe_template import run_from_json + + +def mock_dataframe(n_rows: int = 10) -> pd.DataFrame: + """Return a minimal synthetic DataFrame for fast tests.""" + rng = np.random.default_rng(0) + df = pd.DataFrame({ + "file_name": [f"file_{i}.csv" for i in range(n_rows)], + "CD3D": rng.random(n_rows) * 100, + "FOXP3": rng.random(n_rows) * 50, + "PDL1": rng.random(n_rows) * 75, + }) + # Add some NaN values for testing + df.loc[2, "CD3D"] = np.nan + df.loc[5, "FOXP3"] = np.nan + return df + + +class TestSummarizeDataFrameTemplate(unittest.TestCase): + """Unit tests for the Summarize DataFrame template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.csv" + ) + self.out_file = "summary" + + # Save minimal mock data + mock_dataframe().to_csv(self.in_file, index=False) + + # Minimal parameters - from the example + self.params = { + "Calculate_Centroids": self.in_file, + "Columns": ["file_name", "CD3D", "FOXP3", "PDL1"], + "Print_Missing_Location": True, + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters + with patch('spac.templates.summarize_dataframe_template.' + 'summarize_dataframe') as mock_summarize: + with patch('spac.templates.summarize_dataframe_template.' + 'present_summary_as_figure') as mock_fig: + # Mock the summary dataframe + mock_summarize.return_value = pd.DataFrame( + {"test": [1, 2, 3]} + ) + + # Mock the Plotly figure + mock_plotly_fig = MagicMock() + mock_plotly_fig.write_html = MagicMock() + mock_plotly_fig.show = MagicMock() + mock_fig.return_value = mock_plotly_fig + + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + self.assertIn("summary.html", result) + + # Verify figure methods were called + mock_plotly_fig.show.assert_called_once() + mock_plotly_fig.write_html.assert_called_once_with( + "summary.html" + ) + + # Test 2: Run without saving + with patch('spac.templates.summarize_dataframe_template.' + 'summarize_dataframe') as mock_summarize: + with patch('spac.templates.summarize_dataframe_template.' + 'present_summary_as_figure') as mock_fig: + mock_summarize.return_value = pd.DataFrame( + {"test": [1, 2, 3]} + ) + mock_plotly_fig = MagicMock() + mock_fig.return_value = mock_plotly_fig + + result_no_save = run_from_json( + self.params, save_results=False + ) + # Should return tuple of (figure, dataframe) + self.assertIsInstance(result_no_save, tuple) + self.assertEqual(len(result_no_save), 2) + fig, summary = result_no_save + self.assertEqual(fig, mock_plotly_fig) + self.assertIsInstance(summary, pd.DataFrame) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + with patch('spac.templates.summarize_dataframe_template.' + 'summarize_dataframe') as mock_summarize: + with patch('spac.templates.summarize_dataframe_template.' + 'present_summary_as_figure') as mock_fig: + mock_summarize.return_value = pd.DataFrame( + {"test": [1, 2, 3]} + ) + mock_plotly_fig = MagicMock() + mock_plotly_fig.write_html = MagicMock() + mock_plotly_fig.show = MagicMock() + mock_fig.return_value = mock_plotly_fig + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + @patch('spac.templates.summarize_dataframe_template.' + 'summarize_dataframe') + @patch('spac.templates.summarize_dataframe_template.' + 'present_summary_as_figure') + def test_function_calls(self, mock_present, mock_summarize) -> None: + """Test that main functions are called with correct parameters.""" + # Mock the functions + mock_summary = pd.DataFrame({"test": [1, 2, 3]}) + mock_summarize.return_value = mock_summary + + mock_fig = MagicMock() + mock_fig.write_html = MagicMock() + mock_fig.show = MagicMock() + mock_present.return_value = mock_fig + + run_from_json(self.params) + + # Verify summarize_dataframe was called correctly + mock_summarize.assert_called_once() + call_args = mock_summarize.call_args + + # Check the dataframe was passed + df_arg = call_args[0][0] + self.assertIsInstance(df_arg, pd.DataFrame) + self.assertEqual(len(df_arg), 10) + + # Check keyword arguments + self.assertEqual( + call_args[1]['columns'], + ["file_name", "CD3D", "FOXP3", "PDL1"] + ) + self.assertEqual(call_args[1]['print_nan_locations'], True) + + # Verify present_summary_as_figure was called + mock_present.assert_called_once_with(mock_summary) + + def test_pickle_input(self) -> None: + """Test loading from pickle file.""" + pickle_file = os.path.join(self.tmp_dir.name, "input.pickle") + with open(pickle_file, 'wb') as f: + pickle.dump(mock_dataframe(), f) + + params_pickle = self.params.copy() + params_pickle["Calculate_Centroids"] = pickle_file + + with patch('spac.templates.summarize_dataframe_template.' + 'summarize_dataframe') as mock_summarize: + with patch('spac.templates.summarize_dataframe_template.' + 'present_summary_as_figure') as mock_fig: + mock_summarize.return_value = pd.DataFrame( + {"test": [1, 2, 3]} + ) + mock_plotly_fig = MagicMock() + mock_plotly_fig.write_html = MagicMock() + mock_plotly_fig.show = MagicMock() + mock_fig.return_value = mock_plotly_fig + + result = run_from_json(params_pickle) + self.assertIsInstance(result, dict) + + def test_optional_parameter_defaults(self) -> None: + """Test that optional parameters use correct defaults.""" + params_minimal = { + "Calculate_Centroids": self.in_file, + "Columns": ["file_name", "CD3D"] + # Print_Missing_Location not specified + } + + with patch('spac.templates.summarize_dataframe_template.' + 'summarize_dataframe') as mock_summarize: + with patch('spac.templates.summarize_dataframe_template.' + 'present_summary_as_figure') as mock_fig: + mock_summarize.return_value = pd.DataFrame( + {"test": [1, 2, 3]} + ) + mock_plotly_fig = MagicMock() + mock_plotly_fig.show = MagicMock() + mock_fig.return_value = mock_plotly_fig + + run_from_json(params_minimal, save_results=False) + + # Check default value for Print_Missing_Location + mock_summarize.assert_called_once() + call_args = mock_summarize.call_args + self.assertEqual( + call_args[1]['print_nan_locations'], + False # Default from template JSON + ) + + @patch('builtins.print') + @patch('spac.templates.summarize_dataframe_template.' + 'summarize_dataframe') + def test_console_output(self, mock_summarize, mock_print) -> None: + """Test that NaN locations are printed when requested.""" + # Create summary with NaN info + mock_summary = pd.DataFrame({ + 'column': ['CD3D', 'FOXP3'], + 'missing_indices': [[2], [5]] + }) + mock_summarize.return_value = mock_summary + + with patch('spac.templates.summarize_dataframe_template.' + 'present_summary_as_figure') as mock_fig: + mock_plotly_fig = MagicMock() + mock_plotly_fig.show = MagicMock() + mock_fig.return_value = mock_plotly_fig + + run_from_json(self.params, save_results=False) + + # The summarize_dataframe function itself handles printing + # We just verify it was called with print_nan_locations=True + mock_summarize.assert_called_once() + self.assertTrue( + mock_summarize.call_args[1]['print_nan_locations'] + ) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 30118f9813cc8768401b2206ca1ba867a79175aa Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 10:31:04 -0400 Subject: [PATCH 034/102] fix(summarize_dataframe_template): address comments from copilot --- src/spac/templates/summarize_dataframe_template.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/spac/templates/summarize_dataframe_template.py b/src/spac/templates/summarize_dataframe_template.py index d3a51410..20669876 100644 --- a/src/spac/templates/summarize_dataframe_template.py +++ b/src/spac/templates/summarize_dataframe_template.py @@ -12,13 +12,11 @@ from pathlib import Path from typing import Any, Dict, Union, List, Optional, Tuple import pandas as pd -import matplotlib.pyplot as plt # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) from spac.data_utils import summarize_dataframe -from spac.visualization import present_summary_as_html from spac.visualization import present_summary_as_figure from spac.templates.template_utils import ( load_input, @@ -73,8 +71,8 @@ def run_from_json( df, columns=columns, print_nan_locations=print_missing_location) - # Generate HTML from the summary. - + + # Generate HTML from the summary. fig = present_summary_as_figure(summary) if show_plot: From 829a4bdbc95575079d9e24492f0e6bc2ea57475e Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 11:10:13 -0400 Subject: [PATCH 035/102] feat(combine_annotations_template): add combine_annotations_template function and unit tests --- .../templates/combine_annotations_template.py | 119 ++++++++++ .../test_combine_annotations_template.py | 210 ++++++++++++++++++ 2 files changed, 329 insertions(+) create mode 100644 src/spac/templates/combine_annotations_template.py create mode 100644 tests/templates/test_combine_annotations_template.py diff --git a/src/spac/templates/combine_annotations_template.py b/src/spac/templates/combine_annotations_template.py new file mode 100644 index 00000000..f3fc2a1f --- /dev/null +++ b/src/spac/templates/combine_annotations_template.py @@ -0,0 +1,119 @@ +""" +Platform-agnostic Combine Annotations template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.combine_annotations_template import run_from_json +>>> run_from_json("examples/combine_annotations_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, List + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.data_utils import combine_annotations +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], Any]: + """ + Execute Combine Annotations analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the adata object + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or AnnData + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed AnnData object + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + annotations_list = params["Annotations_Names"] + new_annotation = params.get("New_Annotation_Name", "combined_annotation") + separator = params.get("Separator", "_") + + combine_annotations( + adata, + annotations=annotations_list, + separator=separator, + new_annotation_name=new_annotation + ) + + print("After combining annotations: \n", adata) + value_counts = adata.obs[new_annotation].value_counts(dropna=False) + print(f"Unique labels in {new_annotation}") + print(value_counts) + + # create the frequency CSV for download + df_counts = ( + value_counts + .rename_axis(new_annotation) # move index to a column name + .reset_index(name='count') # two columns: label | count + ) + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "transform_output.pickle") + # Default to pickle format if no recognized extension + if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): + output_file = output_file + '.pickle' + + # Also save the counts CSV + csv_name = f"{new_annotation}_counts.csv" + + saved_files = save_outputs({ + output_file: adata, + csv_name: df_counts + }) + + print(f"\nLabel‑count table written to {csv_name}") + print(f"Combine Annotations completed → {saved_files[output_file]}") + return saved_files + else: + # Return the adata object directly for in-memory workflows + print("Returning AnnData object (not saving to file)") + return adata + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python combine_annotations_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned data object") \ No newline at end of file diff --git a/tests/templates/test_combine_annotations_template.py b/tests/templates/test_combine_annotations_template.py new file mode 100644 index 00000000..76632ee2 --- /dev/null +++ b/tests/templates/test_combine_annotations_template.py @@ -0,0 +1,210 @@ +# tests/templates/test_combine_annotations_template.py +"""Unit tests for the Combine Annotations template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.combine_annotations_template import run_from_json + + +def mock_adata(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "detailed_cell_type": ( + ["B_cell", "T_cell", "NK_cell"] * ((n_cells + 2) // 3) + )[:n_cells], + "broad_cell_type": ( + ["Immune", "Immune", "Immune"] * ((n_cells + 2) // 3) + )[:n_cells] + }) + x_mat = rng.normal(size=(n_cells, 3)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = ["Gene1", "Gene2", "Gene3"] + return adata + + +class TestCombineAnnotationsTemplate(unittest.TestCase): + """Unit tests for the Combine Annotations template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "transform_output" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters matching NIDAP template + self.params = { + "Upstream_Analysis": self.in_file, + "Annotations_Names": ["detailed_cell_type", "broad_cell_type"], + "New_Annotation_Name": "combined_annotation", + "Separator": "_", + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering all input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + # Should have 2 files: pickle and CSV + self.assertEqual(len(result), 2) + # Check pickle file exists + self.assertIn( + f"{self.out_file}.pickle", result + ) + # Check CSV file exists + self.assertIn("combined_annotation_counts.csv", result) + + # Test 2: Run without saving + adata_result = run_from_json(self.params, save_results=False) + # Check we got AnnData back + self.assertIsInstance(adata_result, ad.AnnData) + # Check new annotation was created + self.assertIn("combined_annotation", adata_result.obs.columns) + # Check annotation values are correct format + sample_val = adata_result.obs["combined_annotation"].iloc[0] + self.assertIn("_", sample_val) # Should have separator + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + self.assertEqual(len(result_json), 2) + + # Test 4: Verify CSV content + csv_path = result_json["combined_annotation_counts.csv"] + df_counts = pd.read_csv(csv_path) + # Should have the annotation name and count columns + self.assertIn("combined_annotation", df_counts.columns) + self.assertIn("count", df_counts.columns) + # Should have some rows + self.assertGreater(len(df_counts), 0) + + def test_parameter_validation(self) -> None: + """Test exact error message for missing parameters.""" + params_bad = self.params.copy() + del params_bad["Annotations_Names"] + + with self.assertRaises(KeyError) as context: + run_from_json(params_bad) + + # Check that KeyError mentions the missing parameter + self.assertIn("Annotations_Names", str(context.exception)) + + @patch('spac.templates.combine_annotations_template.combine_annotations') + def test_function_calls(self, mock_combine) -> None: + """Test that main function is called with correct parameters.""" + # Mock the combine_annotations function to add the expected column + def mock_combine_side_effect(adata, **kwargs): + # Simulate what combine_annotations does + annotations = kwargs.get('annotations', []) + separator = kwargs.get('separator', '_') + new_name = kwargs.get('new_annotation_name', 'combined') + + # Create combined values + combined_values = [] + for idx in range(len(adata.obs)): + values = [str(adata.obs[col].iloc[idx]) for col in annotations] + combined_values.append(separator.join(values)) + + adata.obs[new_name] = combined_values + return None + + mock_combine.side_effect = mock_combine_side_effect + + run_from_json(self.params) + + # Verify function was called correctly + mock_combine.assert_called_once() + call_args = mock_combine.call_args + + # Check positional arguments (adata) + self.assertEqual(call_args[0][0].n_obs, 10) + + # Check keyword arguments + self.assertEqual( + call_args[1]['annotations'], + ["detailed_cell_type", "broad_cell_type"] + ) + self.assertEqual(call_args[1]['separator'], "_") + self.assertEqual( + call_args[1]['new_annotation_name'], + "combined_annotation" + ) + + def test_different_separators(self) -> None: + """Test with different separator characters.""" + # Test with hyphen separator + params_hyphen = self.params.copy() + params_hyphen["Separator"] = "-" + params_hyphen["New_Annotation_Name"] = "hyphen_combined" + + adata = run_from_json(params_hyphen, save_results=False) + self.assertIn("hyphen_combined", adata.obs.columns) + # Check that values use hyphen + sample_val = adata.obs["hyphen_combined"].iloc[0] + self.assertIn("-", sample_val) + + # Test with empty separator + params_empty = self.params.copy() + params_empty["Separator"] = "" + params_empty["New_Annotation_Name"] = "concat_combined" + + adata = run_from_json(params_empty, save_results=False) + self.assertIn("concat_combined", adata.obs.columns) + + @patch('builtins.print') + def test_console_output(self, mock_print) -> None: + """Test that expected console output is produced.""" + run_from_json(self.params, save_results=False) + + # Check that print was called with expected messages + print_calls = [str(call[0][0]) for call in mock_print.call_args_list + if call[0]] + + # Should print after combining annotations + after_combining = any( + "After combining annotations:" in msg for msg in print_calls + ) + self.assertTrue(after_combining) + + # Should print unique labels message + unique_labels = any( + "Unique labels in combined_annotation" in msg + for msg in print_calls + ) + self.assertTrue(unique_labels) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 9d8582a88b1d61cd1912aa146153178d8287d82a Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 11:17:43 -0400 Subject: [PATCH 036/102] fix(combine_annotations_template): address comments from copilot CR for combine_annotations_template function --- src/spac/templates/combine_annotations_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spac/templates/combine_annotations_template.py b/src/spac/templates/combine_annotations_template.py index f3fc2a1f..b6a8b3c0 100644 --- a/src/spac/templates/combine_annotations_template.py +++ b/src/spac/templates/combine_annotations_template.py @@ -91,7 +91,7 @@ def run_from_json( csv_name: df_counts }) - print(f"\nLabel‑count table written to {csv_name}") + print(f"\nLabel-count table written to {csv_name}") print(f"Combine Annotations completed → {saved_files[output_file]}") return saved_files else: From 941d641352c6eaee1c293b5ef98f1a9d92646c0c Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 14:44:41 -0400 Subject: [PATCH 037/102] feat(manual_phenotyping_template): add manual_phenotyping_template function and unit tests --- .../templates/manual_phenotyping_template.py | 117 +++++++ .../test_manual_phenotyping_template.py | 290 ++++++++++++++++++ 2 files changed, 407 insertions(+) create mode 100644 src/spac/templates/manual_phenotyping_template.py create mode 100644 tests/templates/test_manual_phenotyping_template.py diff --git a/src/spac/templates/manual_phenotyping_template.py b/src/spac/templates/manual_phenotyping_template.py new file mode 100644 index 00000000..c626d7a2 --- /dev/null +++ b/src/spac/templates/manual_phenotyping_template.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +""" +Platform-agnostic Manual Phenotyping template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.manual_phenotyping_template import run_from_json +>>> run_from_json("examples/manual_phenotyping_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, List, Optional +import pandas as pd + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.phenotyping import assign_manual_phenotypes +from spac.templates.template_utils import ( + save_outputs, + parse_params +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], pd.DataFrame]: + """ + Execute Manual Phenotyping analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the DataFrame + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or DataFrame + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed DataFrame + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load input data - support both DataFrame and CSV file + upstream = params['Upstream_Dataset'] + if isinstance(upstream, pd.DataFrame): + dataframe = upstream # Direct DataFrame pass from previous step + else: + dataframe = pd.read_csv(upstream) # Read from CSV file + + # dataframe = {{{Upstream_Dataset}}} # Already loaded above + phenotypes = pd.read_csv(params['Phenotypes_Code']) + prefix = params.get('Classification_Column_Prefix', '') + suffix = params.get('Classification_Column_Suffix', '') + multiple = params.get('Allow_Multiple_Phenotypes', True) + manual_annotation = params.get('Manual_Annotation_Name', 'manual_phenotype') + + print(phenotypes) + + # returned_dic is not used, but copy from original NIDAP logic + returned_dic = assign_manual_phenotypes( + dataframe, + phenotypes, + prefix=prefix, + suffix=suffix, + annotation=manual_annotation, + multiple=multiple + ) + + # The dataframe changes in place + # --------- Original NIDAP Logic End --------- # + + # Print summary statistics + phenotype_counts = dataframe[manual_annotation].value_counts() + print(f"\nPhenotype distribution:") + print(phenotype_counts) + + print("\nManual Phenotyping completed successfully.") + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "manual_phenotyped.csv") + saved_files = save_outputs({output_file: dataframe}) + + print(f"Manual Phenotyping completed → {saved_files[output_file]}") + + return saved_files + else: + # Return the DataFrame directly for in-memory workflows + print("Returning DataFrame (not saving to file)") + return dataframe + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python manual_phenotyping_template.py ", + file=sys.stderr + ) + sys.exit(1) + + saved_files = run_from_json(sys.argv[1]) + + if isinstance(saved_files, dict): + print("\nOutput files:") + for filename, filepath in saved_files.items(): + print(f" {filename}: {filepath}") \ No newline at end of file diff --git a/tests/templates/test_manual_phenotyping_template.py b/tests/templates/test_manual_phenotyping_template.py new file mode 100644 index 00000000..f30d5641 --- /dev/null +++ b/tests/templates/test_manual_phenotyping_template.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python3 +"""Unit tests for the Manual Phenotyping template.""" + +import json +import os +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch, MagicMock + +import pandas as pd +import numpy as np + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.manual_phenotyping_template import run_from_json + + +def create_mock_data_and_phenotypes(tmp_dir: Path) -> tuple: + """Create minimal data and phenotypes files for testing.""" + # Create mock expression data + n_cells = 20 # Simple scenario as per rule 1 + data = pd.DataFrame({ + 'cell_id': [f'cell_{i}' for i in range(n_cells)], + 'CD3D_expression': np.random.choice([0, 1], n_cells), + 'CD4_expression': np.random.choice([0, 1], n_cells), + 'CD8A_expression': np.random.choice([0, 1], n_cells), + 'FOXP3_expression': np.random.choice([0, 1], n_cells), + 'CD68_expression': np.random.choice([0, 1], n_cells), + 'CD20_expression': np.random.choice([0, 1], n_cells), + 'CD21_expression': np.random.choice([0, 1], n_cells), + 'CD56_expression': np.random.choice([0, 1], n_cells), + }) + data_path = tmp_dir / 'input_data.csv' + data.to_csv(data_path, index=False) + + # Create phenotypes definition + phenotypes = pd.DataFrame({ + 'phenotype_code': [ + 'CD3D+CD4+FOXP3+', + 'CD3D+CD4+', + 'CD3D+CD8A+', + 'CD68+', + 'CD20+' + ], + 'phenotype_name': [ + 'Regulatory T Cell', + 'Helper T Cell', + 'Cytotoxic T Cell', + 'Macrophage', + 'B Cell' + ] + }) + phenotypes_path = tmp_dir / 'phenotypes.csv' + phenotypes.to_csv(phenotypes_path, index=False) + + return data_path, phenotypes_path + + +class TestManualPhenotypingTemplate(unittest.TestCase): + """Unit tests for the Manual Phenotyping template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.tmp_path = Path(self.tmp_dir.name) + + # Create mock files + self.data_path, self.phenotypes_path = \ + create_mock_data_and_phenotypes(self.tmp_path) + + # Minimal parameters + self.params = { + "Upstream_Dataset": str(self.data_path), + "Phenotypes_Code": str(self.phenotypes_path), + "Classification_Column_Prefix": "", + "Classification_Column_Suffix": "_expression", + "Allow_Multiple_Phenotypes": True, + "Manual_Annotation_Name": "manual_phenotype", + "Output_File": "phenotyped_data.csv" + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + @patch('spac.templates.manual_phenotyping_template.' + 'assign_manual_phenotypes') + def test_complete_io_workflow(self, mock_assign) -> None: + """Single I/O test covering all input/output scenarios (Rule 2).""" + # Mock the assign_manual_phenotypes function + def mock_assign_func(df, pheno, **kwargs): + # Add phenotype column with the correct annotation name + annotation_name = kwargs.get('annotation', 'manual_phenotype') + df[annotation_name] = np.random.choice( + ['T Cell', 'B Cell', 'Macrophage', 'no_label'], + len(df) + ) + return {'status': 'success'} + + mock_assign.side_effect = mock_assign_func + + # Change to temp directory for output + original_cwd = os.getcwd() + os.chdir(self.tmp_path) + + try: + # Test 1: Run with save_results=True + saved_files = run_from_json(self.params) + self.assertIn("phenotyped_data.csv", saved_files) + output_path = Path(saved_files["phenotyped_data.csv"]) + self.assertTrue(output_path.exists()) + + # Verify content + result_df = pd.read_csv(output_path) + self.assertEqual(len(result_df), 20) # Same number of cells + self.assertIn('manual_phenotype', result_df.columns) + self.assertIn('cell_id', result_df.columns) + + # Test 2: Run with save_results=False (in-memory) + df_result = run_from_json(self.params, save_results=False) + self.assertIsInstance(df_result, pd.DataFrame) + self.assertEqual(len(df_result), 20) + self.assertIn('manual_phenotype', df_result.columns) + + # Test 3: JSON file input + json_path = self.tmp_path / "params.json" + with open(json_path, "w") as f: + json.dump(self.params, f) + saved_from_json = run_from_json(json_path) + self.assertIn("phenotyped_data.csv", saved_from_json) + + # Test 4: Direct DataFrame input (chained workflow) + input_df = pd.read_csv(self.data_path) + params_df = self.params.copy() + params_df["Upstream_Dataset"] = input_df # Pass DataFrame + + df_from_df = run_from_json(params_df, save_results=False) + self.assertIsInstance(df_from_df, pd.DataFrame) + self.assertEqual(len(df_from_df), 20) + self.assertIn('manual_phenotype', df_from_df.columns) + + # Test 5: Custom parameters + params_custom = self.params.copy() + params_custom["Allow_Multiple_Phenotypes"] = False + params_custom["Manual_Annotation_Name"] = "cell_type" + params_custom["Output_File"] = "custom_output.csv" + + saved_custom = run_from_json(params_custom) + self.assertIn("custom_output.csv", saved_custom) + + # Verify custom annotation name + custom_df = pd.read_csv(saved_custom["custom_output.csv"]) + self.assertIn('cell_type', custom_df.columns) + + # Verify mock was called with correct parameters + call_args = mock_assign.call_args_list[-1] # Last call + self.assertEqual(call_args[1]['multiple'], False) + self.assertEqual(call_args[1]['annotation'], 'cell_type') + + finally: + os.chdir(original_cwd) + + @patch('spac.templates.manual_phenotyping_template.' + 'assign_manual_phenotypes') + def test_error_validation(self, mock_assign) -> None: + """Test exact error messages for various failure scenarios (Rule 3).""" + # Test 1: Missing phenotypes file + params_missing = self.params.copy() + params_missing["Phenotypes_Code"] = str( + self.tmp_path / "missing.csv" + ) + + with self.assertRaises(FileNotFoundError) as context: + run_from_json(params_missing) + + # pandas will raise this error + self.assertIn("No such file or directory", str(context.exception)) + + # Test 2: Missing input data file + params_no_input = self.params.copy() + params_no_input["Upstream_Dataset"] = str( + self.tmp_path / "nonexistent.csv" + ) + + with self.assertRaises(FileNotFoundError) as context: + run_from_json(params_no_input) + + self.assertIn("No such file or directory", str(context.exception)) + + # Test 3: Invalid CSV file for phenotypes + invalid_phenotypes = self.tmp_path / "invalid_phenotypes.csv" + invalid_phenotypes.write_text( + "invalid,csv,content\nwithout proper,structure" + ) + + params_invalid = self.params.copy() + params_invalid["Phenotypes_Code"] = str(invalid_phenotypes) + + # Mock to simulate SPAC function error + mock_assign.side_effect = KeyError( + "phenotype_code column not found" + ) + + with self.assertRaises(KeyError) as context: + run_from_json(params_invalid) + + expected_msg = "phenotype_code column not found" + self.assertEqual(str(context.exception).strip("'"), expected_msg) + + @patch('spac.templates.manual_phenotyping_template.' + 'assign_manual_phenotypes') + @patch('builtins.print') + def test_console_output(self, mock_print, mock_assign) -> None: + """Test that expected messages are printed to console.""" + # Mock the assign function + def mock_assign_func(df, pheno, **kwargs): + df['manual_phenotype'] = ['T Cell'] * len(df) + return {'status': 'success'} + + mock_assign.side_effect = mock_assign_func + + # Change to temp directory + original_cwd = os.getcwd() + os.chdir(self.tmp_path) + + try: + run_from_json(self.params) + + # Check for expected print statements + print_calls = [str(call[0][0]) for call in + mock_print.call_args_list if call[0]] + + # Should print phenotypes DataFrame + phenotypes_printed = any('phenotype_code' in str(call) + for call in print_calls) + self.assertTrue(phenotypes_printed) + + # Should print completion message + completion_msgs = [ + msg for msg in print_calls + if 'Manual Phenotyping completed successfully' in msg + ] + self.assertTrue(len(completion_msgs) > 0) + + # Should print file save message + save_msgs = [ + msg for msg in print_calls + if 'Manual Phenotyping completed →' in msg + ] + self.assertTrue(len(save_msgs) > 0) + + finally: + os.chdir(original_cwd) + + @patch('spac.templates.manual_phenotyping_template.' + 'assign_manual_phenotypes') + def test_phenotype_distribution_output(self, mock_assign) -> None: + """Test that phenotype distribution is correctly calculated/printed.""" + # Create specific phenotype assignments + def mock_assign_func(df, pheno, **kwargs): + # Assign specific phenotypes for testing + phenotypes = ['T Cell'] * 10 + ['B Cell'] * 5 + ['no_label'] * 5 + df[kwargs.get('annotation', 'manual_phenotype')] = phenotypes[:len(df)] + return {'status': 'success'} + + mock_assign.side_effect = mock_assign_func + + with patch('builtins.print') as mock_print: + df_result = run_from_json(self.params, save_results=False) + + # Check distribution in result + counts = df_result['manual_phenotype'].value_counts() + self.assertEqual(counts['T Cell'], 10) + self.assertEqual(counts['B Cell'], 5) + self.assertEqual(counts['no_label'], 5) + + # Check that distribution was printed + print_calls = [str(call[0][0]) for call in + mock_print.call_args_list if call[0]] + distribution_printed = any( + 'Phenotype distribution' in str(call) + for call in print_calls + ) + self.assertTrue(distribution_printed) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From e4d61cf3748e6eb9f3aca7cad9faf2b41b6ea652 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 15:14:57 -0400 Subject: [PATCH 038/102] fix(test_manual_phenotyping_temp): address comments of copilot review for unit tests --- .../test_manual_phenotyping_template.py | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/tests/templates/test_manual_phenotyping_template.py b/tests/templates/test_manual_phenotyping_template.py index f30d5641..13d5ad82 100644 --- a/tests/templates/test_manual_phenotyping_template.py +++ b/tests/templates/test_manual_phenotyping_template.py @@ -22,7 +22,7 @@ def create_mock_data_and_phenotypes(tmp_dir: Path) -> tuple: """Create minimal data and phenotypes files for testing.""" # Create mock expression data - n_cells = 20 # Simple scenario as per rule 1 + n_cells = 20 # Simple scenario with 20 cells for testing data = pd.DataFrame({ 'cell_id': [f'cell_{i}' for i in range(n_cells)], 'CD3D_expression': np.random.choice([0, 1], n_cells), @@ -88,7 +88,7 @@ def tearDown(self) -> None: @patch('spac.templates.manual_phenotyping_template.' 'assign_manual_phenotypes') def test_complete_io_workflow(self, mock_assign) -> None: - """Single I/O test covering all input/output scenarios (Rule 2).""" + """Single I/O test covering all input/output scenarios.""" # Mock the assign_manual_phenotypes function def mock_assign_func(df, pheno, **kwargs): # Add phenotype column with the correct annotation name @@ -165,7 +165,7 @@ def mock_assign_func(df, pheno, **kwargs): @patch('spac.templates.manual_phenotyping_template.' 'assign_manual_phenotypes') def test_error_validation(self, mock_assign) -> None: - """Test exact error messages for various failure scenarios (Rule 3).""" + """Test exact error messages for various failure scenarios.""" # Test 1: Missing phenotypes file params_missing = self.params.copy() params_missing["Phenotypes_Code"] = str( @@ -174,40 +174,26 @@ def test_error_validation(self, mock_assign) -> None: with self.assertRaises(FileNotFoundError) as context: run_from_json(params_missing) - - # pandas will raise this error - self.assertIn("No such file or directory", str(context.exception)) - + # Test 2: Missing input data file params_no_input = self.params.copy() params_no_input["Upstream_Dataset"] = str( self.tmp_path / "nonexistent.csv" ) - with self.assertRaises(FileNotFoundError) as context: + with self.assertRaises(FileNotFoundError): run_from_json(params_no_input) - self.assertIn("No such file or directory", str(context.exception)) - # Test 3: Invalid CSV file for phenotypes - invalid_phenotypes = self.tmp_path / "invalid_phenotypes.csv" - invalid_phenotypes.write_text( - "invalid,csv,content\nwithout proper,structure" - ) - - params_invalid = self.params.copy() - params_invalid["Phenotypes_Code"] = str(invalid_phenotypes) - + # SPAC function error # Mock to simulate SPAC function error - mock_assign.side_effect = KeyError( - "phenotype_code column not found" - ) + mock_assign.side_effect = ValueError("Invalid phenotype code format") - with self.assertRaises(KeyError) as context: - run_from_json(params_invalid) + with self.assertRaises(ValueError) as context: + run_from_json(self.params) - expected_msg = "phenotype_code column not found" - self.assertEqual(str(context.exception).strip("'"), expected_msg) + expected_msg = "Invalid phenotype code format" + self.assertEqual(str(context.exception), expected_msg) @patch('spac.templates.manual_phenotyping_template.' 'assign_manual_phenotypes') From 67e5a802500459e01a918c7fe96d20ab45c373c2 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:13:29 -0400 Subject: [PATCH 039/102] feat(hierarchical_heatmap_template): add hierarchical_heatmap_template and unit tests --- .../hierarchical_heatmap_template.py | 194 ++++++++++++++++ .../test_hierarchical_heatmap_template.py | 209 ++++++++++++++++++ 2 files changed, 403 insertions(+) create mode 100644 src/spac/templates/hierarchical_heatmap_template.py create mode 100644 tests/templates/test_hierarchical_heatmap_template.py diff --git a/src/spac/templates/hierarchical_heatmap_template.py b/src/spac/templates/hierarchical_heatmap_template.py new file mode 100644 index 00000000..ae5f0cec --- /dev/null +++ b/src/spac/templates/hierarchical_heatmap_template.py @@ -0,0 +1,194 @@ +""" +Platform-agnostic Hierarchical Heatmap template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.hierarchical_heatmap_template import run_from_json +>>> run_from_json("examples/hierarchical_heatmap_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, List, Optional, Tuple +import pandas as pd +import matplotlib.pyplot as plt + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.visualization import hierarchical_heatmap +from spac.utils import check_feature +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True, + show_plot: bool = True +) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: + """ + Execute Hierarchical Heatmap analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the figure and + dataframe directly for in-memory workflows. Default is True. + show_plot : bool, optional + Whether to display the plot. Default is True. + + Returns + ------- + dict or tuple + If save_results=True: Dictionary of saved file paths + If save_results=False: Tuple of (figure, dataframe) + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + annotation = params["Annotation"] + layer_to_plot = params.get("Table_to_Visualize", "Original") + features = params.get("Feature_s_", ["All"]) + standard_scale = params.get("Standard_Scale_", "None") + z_score = params.get("Z_Score", "None") + cluster_feature = params.get("Feature_Dendrogram", True) + cluster_annotations = params.get("Annotation_Dendrogram", True) + Figure_Title = params.get("Figure_Title", "Hierarchical Heatmap") + fig_width = params.get("Figure_Width", 8) + fig_height = params.get("Figure_Height", 8) + fig_dpi = params.get("Figure_DPI", 300) + font_size = params.get("Font_Size", 10) + matrix_ratio = params.get("Matrix_Plot_Ratio", 0.8) + swap_axes = params.get("Swap_Axes", False) + rotate_label = params.get("Rotate_Label_", False) + r_h_axis_dengrogram = params.get( + "Horizontal_Dendrogram_Display_Ratio", 0.2 + ) + r_v_axis_dengrogram = params.get( + "Vertical_Dendrogram_Display_Ratio", 0.2 + ) + v_min = params.get("Value_Min", "None") + v_max = params.get("Value_Max", "None") + color_map = params.get("Color_Map", 'seismic') + + # Use check_feature to validate features + if len(features) == 1 and features[0] == "All": + features = None + else: + check_feature(adata, features) + + if not swap_axes: + features = None + + # Use text_to_value for parameter conversions + standard_scale = text_to_value( + standard_scale, to_int=True, param_name='Standard Scale' + ) + layer_to_plot = text_to_value( + layer_to_plot, default_none_text="Original" + ) + z_score = text_to_value(z_score, param_name='Z Score') + vmin = text_to_value( + v_min, default_none_text="none", to_float=True, + param_name="Value Min" + ) + vmax = text_to_value( + v_max, default_none_text="none", to_float=True, + param_name="Value Max" + ) + + fig, ax = plt.subplots() + plt.rcParams.update({'font.size': font_size}) + fig.set_size_inches(fig_width, fig_height) + fig.set_dpi(fig_dpi) + + mean_intensity, clustergrid, dendrogram_data = hierarchical_heatmap( + adata, + annotation=annotation, + features=features, + layer=layer_to_plot, + cluster_feature=cluster_feature, + cluster_annotations=cluster_annotations, + standard_scale=standard_scale, + z_score=z_score, + swap_axes=swap_axes, + rotate_label=rotate_label, + figsize=(fig_width, fig_height), + dendrogram_ratio=(r_h_axis_dengrogram, r_v_axis_dengrogram), + vmin=vmin, + vmax=vmax, + cmap=color_map + ) + print("Printing mean intensity data.") + print(mean_intensity) + print() + print("Printing dendrogram data.") + for data in dendrogram_data: + print(data) + print(dendrogram_data[data]) + + # Ensure the mean_intensity index matches phenograph clusters + row_clusters = adata.obs[annotation].astype(str).unique() + mean_intensity[annotation] = mean_intensity.index.astype(str) + + # Reorder columns to move 'clusters' to the first position + cols = mean_intensity.columns.tolist() + cols = [annotation] + [col for col in cols if col != annotation] + mean_intensity = mean_intensity[cols] + + # Show the modified plot + clustergrid.ax_heatmap.set_title(Figure_Title) + clustergrid.height = fig_height * matrix_ratio + clustergrid.width = fig_width * matrix_ratio + plt.close(1) + + if show_plot: + plt.show() + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "plots.csv") + saved_files = save_outputs({output_file: mean_intensity}) + + print( + f"Hierarchical Heatmap completed → " + f"{saved_files[output_file]}" + ) + return saved_files + else: + # Return the figure and dataframe directly for in-memory workflows + print("Returning figure and dataframe (not saving to file)") + return clustergrid.fig, mean_intensity + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python hierarchical_heatmap_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned figure and dataframe") \ No newline at end of file diff --git a/tests/templates/test_hierarchical_heatmap_template.py b/tests/templates/test_hierarchical_heatmap_template.py new file mode 100644 index 00000000..fabd7496 --- /dev/null +++ b/tests/templates/test_hierarchical_heatmap_template.py @@ -0,0 +1,209 @@ +# tests/templates/test_hierarchical_heatmap_template.py +"""Unit tests for the Hierarchical Heatmap template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import matplotlib +matplotlib.use("Agg") # Headless backend for CI + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.hierarchical_heatmap_template import run_from_json + + +def mock_adata(n_cells: int = 100) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "phenograph_k60_r1": ( + ["Cluster1", "Cluster2", "Cluster3"] * ((n_cells + 2) // 3) + )[:n_cells], + "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells] + }) + # Create expression data with 9 markers as in the example + x_mat = rng.normal(size=(n_cells, 9)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = [ + "Hif1a", "NOS2", "COX2", "β-catenin", "vimentin", + "E-cadherin", "Ki67", "PIMO", "aSMA" + ] + # Add a z-score normalized layer for testing + adata.layers["arcsinh_z_scores"] = ( + (x_mat - x_mat.mean(axis=0)) / x_mat.std(axis=0) + ) + return adata + + +class TestHierarchicalHeatmapTemplate(unittest.TestCase): + """Unit tests for the Hierarchical Heatmap template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "mean_intensity.csv" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters - from the JSON template + self.params = { + "Upstream_Analysis": self.in_file, + "Annotation": "phenograph_k60_r1", + "Table_to_Visualize": "arcsinh_z_scores", + "Feature_s_": ["All"], + "Standard_Scale_": "None", + "Z_Score": "None", + "Feature_Dendrogram": True, + "Annotation_Dendrogram": True, + "Figure_Title": "Hierarchical Heatmap", + "Figure_Width": 15, + "Figure_Height": 12, + "Figure_DPI": 300, + "Font_Size": 14, + "Matrix_Plot_Ratio": 0.8, + "Swap_Axes": False, + "Rotate_Label_": False, + "Horizontal_Dendrogram_Display_Ratio": 0.2, + "Vertical_Dendrogram_Display_Ratio": 0.2, + "Value_Min": "-3", + "Value_Max": "3", + "Color_Map": "seismic", + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Mock the hierarchical_heatmap function + mock_clustergrid = MagicMock() + mock_clustergrid.fig = MagicMock() + mock_clustergrid.ax_heatmap = MagicMock() + mock_clustergrid.height = 12 + mock_clustergrid.width = 15 + + mock_mean_intensity = pd.DataFrame({ + 'Hif1a': [0.1, 0.2, 0.3], + 'NOS2': [0.4, 0.5, 0.6], + 'COX2': [0.7, 0.8, 0.9] + }, index=['Cluster1', 'Cluster2', 'Cluster3']) + + mock_dendrogram_data = { + 'row_dendrogram': {'data': 'row_data'}, + 'col_dendrogram': {'data': 'col_data'} + } + + with patch( + 'spac.templates.hierarchical_heatmap_template.' + 'hierarchical_heatmap', + return_value=( + mock_mean_intensity, mock_clustergrid, + mock_dendrogram_data + ) + ): + + # Test 1: Run with default parameters + result = run_from_json(self.params, show_plot=False) + self.assertIsInstance(result, dict) + self.assertIn(self.out_file, result) + + # Test 2: Run without saving + result_no_save = run_from_json( + self.params, save_results=False, show_plot=False + ) + # Check appropriate return type based on template + self.assertIsInstance(result_no_save, tuple) + self.assertEqual(len(result_no_save), 2) + + # Test 3: JSON file input + json_path = os.path.join( + self.tmp_dir.name, "params.json" + ) + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path, show_plot=False) + self.assertIsInstance(result_json, dict) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test invalid standard scale conversion + params_bad = self.params.copy() + params_bad["Standard_Scale_"] = "invalid_number" + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad, show_plot=False) + + # Check exact error message + expected_msg = ( + "Error: can't convert Standard Scale to integer. " + "Received:\"invalid_number\"" + ) + self.assertEqual(str(context.exception), expected_msg) + + @patch('spac.templates.hierarchical_heatmap_template.' + 'hierarchical_heatmap') + def test_function_calls(self, mock_heatmap) -> None: + """Test that main function is called with correct parameters.""" + # Mock the main function + mock_clustergrid = MagicMock() + mock_clustergrid.fig = MagicMock() + mock_clustergrid.ax_heatmap = MagicMock() + + # Create a proper dataframe with matching dimensions + mock_mean_intensity = pd.DataFrame({ + 'Hif1a': [0.1, 0.2, 0.3], + 'NOS2': [0.4, 0.5, 0.6], + 'COX2': [0.7, 0.8, 0.9] + }, index=['Cluster1', 'Cluster2', 'Cluster3']) + + mock_heatmap.return_value = ( + mock_mean_intensity, mock_clustergrid, {} + ) + + # Test with swap_axes=True to verify features handling + params_swap = self.params.copy() + params_swap["Swap_Axes"] = True + params_swap["Feature_s_"] = ["Hif1a", "NOS2"] + + run_from_json(params_swap, save_results=False, show_plot=False) + + # Verify function was called correctly + mock_heatmap.assert_called_once() + call_kwargs = mock_heatmap.call_args[1] + + # Check specific parameter conversions + self.assertEqual( + call_kwargs['annotation'], "phenograph_k60_r1" + ) + self.assertEqual(call_kwargs['layer'], "arcsinh_z_scores") + self.assertEqual(call_kwargs['features'], ["Hif1a", "NOS2"]) + self.assertEqual(call_kwargs['swap_axes'], True) + self.assertEqual(call_kwargs['vmin'], -3.0) + self.assertEqual(call_kwargs['vmax'], 3.0) + self.assertEqual(call_kwargs['cmap'], "seismic") + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 6466e2f8daa4c95c49e0b6d0b0e5ec1460064d09 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:45:01 -0400 Subject: [PATCH 040/102] feat(hierarchical_heatmap_template): add hierarchical_heatmap_template function and unit tests --- .../templates/hierarchical_heatmap_template.py | 16 ++++++++-------- .../test_hierarchical_heatmap_template.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/spac/templates/hierarchical_heatmap_template.py b/src/spac/templates/hierarchical_heatmap_template.py index ae5f0cec..8bee4958 100644 --- a/src/spac/templates/hierarchical_heatmap_template.py +++ b/src/spac/templates/hierarchical_heatmap_template.py @@ -10,7 +10,7 @@ import json import sys from pathlib import Path -from typing import Any, Dict, Union, List, Optional, Tuple +from typing import Any, Dict, Union, List, Optional import pandas as pd import matplotlib.pyplot as plt @@ -31,7 +31,7 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], save_results: bool = True, show_plot: bool = True -) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: +) -> Union[Dict[str, str], pd.DataFrame]: """ Execute Hierarchical Heatmap analysis with parameters from JSON. Replicates the NIDAP template functionality exactly. @@ -48,9 +48,9 @@ def run_from_json( Returns ------- - dict or tuple + dict or DataFrame If save_results=True: Dictionary of saved file paths - If save_results=False: Tuple of (figure, dataframe) + If save_results=False: The mean intensity dataframe """ # Parse parameters from JSON params = parse_params(json_path) @@ -74,10 +74,10 @@ def run_from_json( matrix_ratio = params.get("Matrix_Plot_Ratio", 0.8) swap_axes = params.get("Swap_Axes", False) rotate_label = params.get("Rotate_Label_", False) - r_h_axis_dengrogram = params.get( + r_h_axis_dendrogram = params.get( "Horizontal_Dendrogram_Display_Ratio", 0.2 ) - r_v_axis_dengrogram = params.get( + r_v_axis_dendrogram = params.get( "Vertical_Dendrogram_Display_Ratio", 0.2 ) v_min = params.get("Value_Min", "None") @@ -127,7 +127,7 @@ def run_from_json( swap_axes=swap_axes, rotate_label=rotate_label, figsize=(fig_width, fig_height), - dendrogram_ratio=(r_h_axis_dengrogram, r_v_axis_dengrogram), + dendrogram_ratio=(r_h_axis_dendrogram, r_v_axis_dendrogram), vmin=vmin, vmax=vmax, cmap=color_map @@ -172,7 +172,7 @@ def run_from_json( else: # Return the figure and dataframe directly for in-memory workflows print("Returning figure and dataframe (not saving to file)") - return clustergrid.fig, mean_intensity + return mean_intensity # CLI interface diff --git a/tests/templates/test_hierarchical_heatmap_template.py b/tests/templates/test_hierarchical_heatmap_template.py index fabd7496..fac7d38d 100644 --- a/tests/templates/test_hierarchical_heatmap_template.py +++ b/tests/templates/test_hierarchical_heatmap_template.py @@ -133,8 +133,8 @@ def test_complete_io_workflow(self) -> None: self.params, save_results=False, show_plot=False ) # Check appropriate return type based on template - self.assertIsInstance(result_no_save, tuple) - self.assertEqual(len(result_no_save), 2) + self.assertIsInstance(result_no_save, pd.DataFrame) + self.assertEqual(len(result_no_save), 3) # 3 clusters # Test 3: JSON file input json_path = os.path.join( From 743fb10f71dc015b8892a4b291d3a2a9069e89a2 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 17:18:08 -0400 Subject: [PATCH 041/102] feat(utag_clustering_template): add utag_clustering_template and unit tests --- .../templates/utag_clustering_template.py | 165 +++++++++++++++++ .../test_utag_clustering_template.py | 169 ++++++++++++++++++ 2 files changed, 334 insertions(+) create mode 100644 src/spac/templates/utag_clustering_template.py create mode 100644 tests/templates/test_utag_clustering_template.py diff --git a/src/spac/templates/utag_clustering_template.py b/src/spac/templates/utag_clustering_template.py new file mode 100644 index 00000000..c84dd76a --- /dev/null +++ b/src/spac/templates/utag_clustering_template.py @@ -0,0 +1,165 @@ +""" +Platform-agnostic UTAG Clustering template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.utag_clustering_template import run_from_json +>>> run_from_json("examples/utag_clustering_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union +import pandas as pd + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.transformations import run_utag_clustering +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], Any]: + """ + Execute UTAG Clustering analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the adata object + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or AnnData + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed AnnData object + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + layer = params.get("Table_to_Process", "Original") + features = params.get("Features", ["All"]) + slide = params.get("Slide_Annotation", "None") + Distance_threshold = params.get("Distance_Threshold", 20.0) + K_neighbors = params.get("K_Nearest_Neighbors", 15) + resolution = params.get("Resolution_Parameter", 1) + principal_components = params.get("PCA_Components", "None") + random_seed = params.get("Random_Seed", 42) + n_jobs = params.get("N_Jobs", 1) + N_iterations = params.get("Leiden_Iterations", 5) + Parallel_processes = params.get("Parellel_Processes", False) + output_annotation = params.get("Output_Annotation_Name", "UTAG") + + # layer: convert "Original" → None + layer_arg = None if layer.lower().strip() == "original" else layer + + # features: ["All"] → None, else leave list and print selection + if isinstance(features, list) and any(item == "All" for item in features): + print("Clustering all features") + features_arg = None + else: + feature_str = "\n".join(features) + print(f"Clustering features:\n{feature_str}") + features_arg = features + + # slide: "None" → None + slide_arg = text_to_value( + slide, + default_none_text="None", + value_to_convert_to=None + ) + + # principal_components: "None" or integer string → None or int + principal_components_arg = text_to_value( + principal_components, + default_none_text="None", + value_to_convert_to=None, + to_int=True, + param_name="principal_components" + ) + + print("\nBefore UTAG Clustering: \n", adata) + + run_utag_clustering( + adata, + features = features_arg, + k = K_neighbors, + resolution = resolution, + max_dist = Distance_threshold, + n_pcs = principal_components_arg, + random_state = random_seed, + n_jobs = n_jobs, + n_iterations = N_iterations, + slide_key = slide_arg, + layer = layer_arg, + output_annotation = output_annotation, + parallel = Parallel_processes, + ) + + print("\nAfter UTAG Clustering: \n", + adata) + + print("\nUTAG Cluster Count: \n", + len(adata.obs[output_annotation].unique().tolist())) + + print("\nUTAG Cluster Names: \n", + adata.obs[output_annotation].unique().tolist()) + + # Count and display occurrences of each label in the annotation + print(f'\nCount of cells in the output annotation:"{output_annotation}":') + label_counts = adata.obs[output_annotation].value_counts() + print(label_counts) + print("\n") + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "transform_output.pickle") + # Default to pickle format if no recognized extension + if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): + output_file = output_file + '.pickle' + + saved_files = save_outputs({output_file: adata}) + + print(f"UTAG Clustering completed → {saved_files[output_file]}") + return saved_files + else: + # Return the adata object directly for in-memory workflows + print("Returning AnnData object (not saving to file)") + return adata + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python utag_clustering_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned AnnData object") \ No newline at end of file diff --git a/tests/templates/test_utag_clustering_template.py b/tests/templates/test_utag_clustering_template.py new file mode 100644 index 00000000..2a49317f --- /dev/null +++ b/tests/templates/test_utag_clustering_template.py @@ -0,0 +1,169 @@ +# tests/templates/test_utag_clustering_template.py +"""Unit tests for the UTAG Clustering template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.utag_clustering_template import run_from_json + + +def mock_adata(n_cells: int = 50) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], + "slide_id": (["Slide1", "Slide2"] * ((n_cells + 1) // 2))[:n_cells] + }) + x_mat = rng.normal(size=(n_cells, 5)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = ["Marker1", "Marker2", "Marker3", "Marker4", "Marker5"] + # Add spatial coordinates required for UTAG + adata.obsm["spatial"] = rng.random((n_cells, 2)) * 100 + return adata + + +class TestUTAGClusteringTemplate(unittest.TestCase): + """Unit tests for the UTAG Clustering template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "output" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters - adjust based on template + self.params = { + "Upstream_Analysis": self.in_file, + "Table_to_Process": "Original", + "Features": ["All"], + "Slide_Annotation": "None", + "Distance_Threshold": 20.0, + "K_Nearest_Neighbors": 15, + "Resolution_Parameter": 1, + "PCA_Components": "None", + "Random_Seed": 42, + "N_Jobs": 1, + "Leiden_Iterations": 5, + "Parellel_Processes": False, + "Output_Annotation_Name": "UTAG", + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Mock the run_utag_clustering function + with patch('spac.templates.utag_clustering_template.' + 'run_utag_clustering') as mock_utag: + # Mock function adds UTAG column to adata.obs + def add_utag_column(adata, **kwargs): + # Add mock UTAG clusters + n_cells = adata.n_obs + clusters = ["UTAG_" + str(i % 3) + for i in range(n_cells)] + adata.obs[kwargs['output_annotation']] = \ + pd.Categorical(clusters) + + mock_utag.side_effect = add_utag_column + + # Test 1: Run with default parameters + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + self.assertTrue(len(result) > 0) + + # Test 2: Run without saving + result_no_save = run_from_json( + self.params, save_results=False + ) + # Check appropriate return type based on template + self.assertIsInstance(result_no_save, ad.AnnData) + self.assertIn("UTAG", result_no_save.obs.columns) + + # Test 3: JSON file input + json_path = os.path.join( + self.tmp_dir.name, "params.json" + ) + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test invalid integer conversion for principal_components + params_bad = self.params.copy() + params_bad["PCA_Components"] = "invalid_number" + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + # Check exact error message + expected_msg = ( + "Error: can't convert principal_components to integer. " + "Received:\"invalid_number\"" + ) + self.assertEqual(str(context.exception), expected_msg) + + @patch('spac.templates.utag_clustering_template.run_utag_clustering') + def test_function_calls(self, mock_utag) -> None: + """Test that main function is called with correct parameters.""" + # Mock the main function + def add_utag_column(adata, **kwargs): + adata.obs[kwargs['output_annotation']] = pd.Categorical( + ["UTAG_0"] * adata.n_obs + ) + + mock_utag.side_effect = add_utag_column + + # Test with specific features instead of "All" + params_features = self.params.copy() + params_features["Features"] = ["Marker1", "Marker3"] + params_features["Slide_Annotation"] = "slide_id" + params_features["PCA_Components"] = "10" + + run_from_json(params_features, save_results=False) + + # Verify function was called correctly + mock_utag.assert_called_once() + call_args = mock_utag.call_args + + # Check parameter conversions + self.assertEqual(call_args[1]['features'], ["Marker1", "Marker3"]) + self.assertEqual(call_args[1]['k'], 15) + self.assertEqual(call_args[1]['resolution'], 1) + self.assertEqual(call_args[1]['max_dist'], 20.0) + self.assertEqual(call_args[1]['n_pcs'], 10) + self.assertEqual(call_args[1]['slide_key'], "slide_id") + self.assertEqual(call_args[1]['layer'], None) # "Original" → None + self.assertEqual(call_args[1]['output_annotation'], "UTAG") + self.assertEqual(call_args[1]['parallel'], False) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 6da39852b1d2ee8b3a1b4fda5c040055d6ae3cd4 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 17:52:37 -0400 Subject: [PATCH 042/102] feat(utag_clustering_template): add utag_clustering_template and unit tests --- .../templates/utag_clustering_template.py | 48 +++++++++++-------- .../test_utag_clustering_template.py | 2 +- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/spac/templates/utag_clustering_template.py b/src/spac/templates/utag_clustering_template.py index c84dd76a..2a20f7a2 100644 --- a/src/spac/templates/utag_clustering_template.py +++ b/src/spac/templates/utag_clustering_template.py @@ -71,7 +71,9 @@ def run_from_json( layer_arg = None if layer.lower().strip() == "original" else layer # features: ["All"] → None, else leave list and print selection - if isinstance(features, list) and any(item == "All" for item in features): + if isinstance(features, list) and any( + item == "All" for item in features + ): print("Clustering all features") features_arg = None else: @@ -99,31 +101,37 @@ def run_from_json( run_utag_clustering( adata, - features = features_arg, - k = K_neighbors, - resolution = resolution, - max_dist = Distance_threshold, - n_pcs = principal_components_arg, - random_state = random_seed, - n_jobs = n_jobs, - n_iterations = N_iterations, - slide_key = slide_arg, - layer = layer_arg, - output_annotation = output_annotation, - parallel = Parallel_processes, + features=features_arg, + k=K_neighbors, + resolution=resolution, + max_dist=Distance_threshold, + n_pcs=principal_components_arg, + random_state=random_seed, + n_jobs=n_jobs, + n_iterations=N_iterations, + slide_key=slide_arg, + layer=layer_arg, + output_annotation=output_annotation, + parallel=Parallel_processes, ) - print("\nAfter UTAG Clustering: \n", - adata) + print("\nAfter UTAG Clustering: \n", adata) - print("\nUTAG Cluster Count: \n", - len(adata.obs[output_annotation].unique().tolist())) + print( + "\nUTAG Cluster Count: \n", + len(adata.obs[output_annotation].unique().tolist()) + ) - print("\nUTAG Cluster Names: \n", - adata.obs[output_annotation].unique().tolist()) + print( + "\nUTAG Cluster Names: \n", + adata.obs[output_annotation].unique().tolist() + ) # Count and display occurrences of each label in the annotation - print(f'\nCount of cells in the output annotation:"{output_annotation}":') + print( + f'\nCount of cells in the output annotation:' + f'"{output_annotation}":' + ) label_counts = adata.obs[output_annotation].value_counts() print(label_counts) print("\n") diff --git a/tests/templates/test_utag_clustering_template.py b/tests/templates/test_utag_clustering_template.py index 2a49317f..c734eafb 100644 --- a/tests/templates/test_utag_clustering_template.py +++ b/tests/templates/test_utag_clustering_template.py @@ -64,7 +64,7 @@ def setUp(self) -> None: "Random_Seed": 42, "N_Jobs": 1, "Leiden_Iterations": 5, - "Parellel_Processes": False, + "Parallel_Processes": False, "Output_Annotation_Name": "UTAG", "Output_File": self.out_file, } From e79fd7814a8728c9f9a529e90256ac44726d1571 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 18:14:31 -0400 Subject: [PATCH 043/102] feat(umap_transformation_template): add umap_transformation_template function and unit tests --- .../templates/umap_transformation_template.py | 118 ++++++++++++ .../test_umap_transformation_template.py | 172 ++++++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 src/spac/templates/umap_transformation_template.py create mode 100644 tests/templates/test_umap_transformation_template.py diff --git a/src/spac/templates/umap_transformation_template.py b/src/spac/templates/umap_transformation_template.py new file mode 100644 index 00000000..bce80b26 --- /dev/null +++ b/src/spac/templates/umap_transformation_template.py @@ -0,0 +1,118 @@ +""" +Platform-agnostic UMAP transformation template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.umap_transformation_template import run_from_json +>>> run_from_json("examples/umap_transformation_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union +import pickle + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +# Import SPAC functions from NIDAP template +from spac.transformations import run_umap +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], Any]: + """ + Execute UMAP transformation analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the adata object + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or AnnData + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed AnnData object + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters - Note: HPC parameters are ignored in SPAC version + n_neighbors = params.get("Number_of_Neighbors", 75) + min_dist = params.get("Minimum_Distance_between_Points", 0.1) + n_components = params.get("Target_Dimension_Number", 2) + metric = params.get("Computational_Metric", "euclidean") + random_state = params.get("Random_State", 0) + transform_seed = params.get("Transform_Seed", 42) + layer = params.get("Table_to_Process", "Original") + + if layer == "Original": + layer = None + + udpated_dataset = run_umap( + adata=adata, + n_neighbors=n_neighbors, + min_dist=min_dist, + n_components=n_components, + metric=metric, + random_state=random_state, + transform_seed=transform_seed, + layer=layer, + verbose=True + ) + + # Print adata info as in NIDAP + print(adata) + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "transform_output.pickle") + # Default to pickle format if no recognized extension + if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): + output_file = output_file + '.pickle' + + saved_files = save_outputs({output_file: udpated_dataset}) + + print(f"UMAP transformation completed → {saved_files[output_file]}") + return saved_files + else: + # Return the adata object directly for in-memory workflows + print("Returning AnnData object (not saving to file)") + return udpated_dataset + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + f"Usage: python umap_transformation_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned data object") \ No newline at end of file diff --git a/tests/templates/test_umap_transformation_template.py b/tests/templates/test_umap_transformation_template.py new file mode 100644 index 00000000..b21430da --- /dev/null +++ b/tests/templates/test_umap_transformation_template.py @@ -0,0 +1,172 @@ +# tests/templates/test_umap_transformation_template.py +"""Unit tests for the UMAP transformation template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.umap_transformation_template import run_from_json + + +def mock_adata(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], + "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] + }) + x_mat = rng.normal(size=(n_cells, 5)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = [f"Gene{i}" for i in range(5)] + # Add a layer to test layer processing + adata.layers["arcsinh_z_scores"] = rng.normal(size=(n_cells, 5)) + return adata + + +class TestUmapTransformationTemplate(unittest.TestCase): + """Unit tests for the UMAP transformation template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "output" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters from NIDAP template + self.params = { + "Upstream_Analysis": self.in_file, + "Number_of_Neighbors": 5, # Small for fast test + "Minimum_Distance_between_Points": 0.1, + "Target_Dimension_Number": 2, + "Computational_Metric": "euclidean", + "Random_State": 0, + "Transform_Seed": 42, + "Table_to_Process": "Original", + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters + with patch('spac.templates.umap_transformation_template.' + 'run_umap') as mock_umap: + # Mock the run_umap function + mock_adata_obj = mock_adata() + mock_adata_obj.obsm["X_umap"] = np.random.rand(10, 2) + mock_umap.return_value = mock_adata_obj + + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + # Verify file was saved + self.assertTrue(len(result) > 0) + + # Verify run_umap was called with correct parameters + mock_umap.assert_called_once() + call_kwargs = mock_umap.call_args[1] + self.assertEqual(call_kwargs['n_neighbors'], 5) + self.assertEqual(call_kwargs['min_dist'], 0.1) + self.assertEqual(call_kwargs['n_components'], 2) + self.assertEqual(call_kwargs['metric'], 'euclidean') + self.assertEqual(call_kwargs['random_state'], 0) + self.assertEqual(call_kwargs['transform_seed'], 42) + self.assertIsNone(call_kwargs['layer']) # "Original" → None + self.assertTrue(call_kwargs['verbose']) + + # Test 2: Run without saving + with patch('spac.templates.umap_transformation_template.' + 'run_umap') as mock_umap: + mock_adata_obj = mock_adata() + mock_adata_obj.obsm["X_umap"] = np.random.rand(10, 2) + mock_umap.return_value = mock_adata_obj + + result_no_save = run_from_json( + self.params, save_results=False + ) + # Check appropriate return type based on template + self.assertIsInstance(result_no_save, ad.AnnData) + self.assertIn("X_umap", result_no_save.obsm) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + with patch('spac.templates.umap_transformation_template.' + 'run_umap') as mock_umap: + mock_adata_obj = mock_adata() + mock_adata_obj.obsm["X_umap"] = np.random.rand(10, 2) + mock_umap.return_value = mock_adata_obj + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + def test_layer_parameter_handling(self) -> None: + """Test that layer parameter is handled correctly.""" + # Test with specific layer + params_with_layer = self.params.copy() + params_with_layer["Table_to_Process"] = "arcsinh_z_scores" + + with patch('spac.templates.umap_transformation_template.' + 'run_umap') as mock_umap: + mock_adata_obj = mock_adata() + mock_adata_obj.obsm["X_umap"] = np.random.rand(10, 2) + mock_umap.return_value = mock_adata_obj + + run_from_json(params_with_layer, save_results=False) + + # Verify layer parameter was passed correctly + call_kwargs = mock_umap.call_args[1] + self.assertEqual(call_kwargs['layer'], "arcsinh_z_scores") + + def test_default_parameters(self) -> None: + """Test that default parameters from JSON template are used.""" + # Minimal params - only required fields + minimal_params = { + "Upstream_Analysis": self.in_file, + } + + with patch('spac.templates.umap_transformation_template.' + 'run_umap') as mock_umap: + mock_adata_obj = mock_adata() + mock_adata_obj.obsm["X_umap"] = np.random.rand(10, 2) + mock_umap.return_value = mock_adata_obj + + run_from_json(minimal_params, save_results=False) + + # Verify defaults from JSON template were used + call_kwargs = mock_umap.call_args[1] + self.assertEqual(call_kwargs['n_neighbors'], 75) + self.assertEqual(call_kwargs['min_dist'], 0.1) + self.assertEqual(call_kwargs['n_components'], 2) + self.assertEqual(call_kwargs['metric'], 'euclidean') + self.assertEqual(call_kwargs['random_state'], 0) + self.assertEqual(call_kwargs['transform_seed'], 42) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 9d24638e5a235f3e128e72fc1e11f28f52bb1822 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 18:51:03 -0400 Subject: [PATCH 044/102] fix(umap_transformation_template): return adata in place and fix comments of copilot --- .../templates/umap_transformation_template.py | 8 ++-- .../test_umap_transformation_template.py | 46 ++++++++++++------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/spac/templates/umap_transformation_template.py b/src/spac/templates/umap_transformation_template.py index bce80b26..732d83fb 100644 --- a/src/spac/templates/umap_transformation_template.py +++ b/src/spac/templates/umap_transformation_template.py @@ -66,7 +66,7 @@ def run_from_json( if layer == "Original": layer = None - udpated_dataset = run_umap( + updated_dataset = run_umap( adata=adata, n_neighbors=n_neighbors, min_dist=min_dist, @@ -89,14 +89,16 @@ def run_from_json( if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): output_file = output_file + '.pickle' - saved_files = save_outputs({output_file: udpated_dataset}) + # Note: Following NIDAP pattern, save the transformed object + # (which is the same as adata if run_umap modifies in-place) + saved_files = save_outputs({output_file: updated_dataset}) print(f"UMAP transformation completed → {saved_files[output_file]}") return saved_files else: # Return the adata object directly for in-memory workflows print("Returning AnnData object (not saving to file)") - return udpated_dataset + return adata # CLI interface diff --git a/tests/templates/test_umap_transformation_template.py b/tests/templates/test_umap_transformation_template.py index b21430da..cf58784a 100644 --- a/tests/templates/test_umap_transformation_template.py +++ b/tests/templates/test_umap_transformation_template.py @@ -75,10 +75,12 @@ def test_complete_io_workflow(self) -> None: # Test 1: Run with default parameters with patch('spac.templates.umap_transformation_template.' 'run_umap') as mock_umap: - # Mock the run_umap function - mock_adata_obj = mock_adata() - mock_adata_obj.obsm["X_umap"] = np.random.rand(10, 2) - mock_umap.return_value = mock_adata_obj + # Mock run_umap to modify in-place and return the same object + def mock_run_umap_inplace(adata, **kwargs): + adata.obsm["X_umap"] = np.random.rand(adata.n_obs, 2) + return adata + + mock_umap.side_effect = mock_run_umap_inplace result = run_from_json(self.params) self.assertIsInstance(result, dict) @@ -100,9 +102,12 @@ def test_complete_io_workflow(self) -> None: # Test 2: Run without saving with patch('spac.templates.umap_transformation_template.' 'run_umap') as mock_umap: - mock_adata_obj = mock_adata() - mock_adata_obj.obsm["X_umap"] = np.random.rand(10, 2) - mock_umap.return_value = mock_adata_obj + # Mock run_umap to modify in-place and return the same object + def mock_run_umap_inplace(adata, **kwargs): + adata.obsm["X_umap"] = np.random.rand(adata.n_obs, 2) + return adata + + mock_umap.side_effect = mock_run_umap_inplace result_no_save = run_from_json( self.params, save_results=False @@ -118,9 +123,12 @@ def test_complete_io_workflow(self) -> None: with patch('spac.templates.umap_transformation_template.' 'run_umap') as mock_umap: - mock_adata_obj = mock_adata() - mock_adata_obj.obsm["X_umap"] = np.random.rand(10, 2) - mock_umap.return_value = mock_adata_obj + # Mock run_umap to modify in-place + def mock_run_umap_inplace(adata, **kwargs): + adata.obsm["X_umap"] = np.random.rand(adata.n_obs, 2) + return adata + + mock_umap.side_effect = mock_run_umap_inplace result_json = run_from_json(json_path) self.assertIsInstance(result_json, dict) @@ -133,9 +141,12 @@ def test_layer_parameter_handling(self) -> None: with patch('spac.templates.umap_transformation_template.' 'run_umap') as mock_umap: - mock_adata_obj = mock_adata() - mock_adata_obj.obsm["X_umap"] = np.random.rand(10, 2) - mock_umap.return_value = mock_adata_obj + # Mock run_umap to modify in-place + def mock_run_umap_inplace(adata, **kwargs): + adata.obsm["X_umap"] = np.random.rand(adata.n_obs, 2) + return adata + + mock_umap.side_effect = mock_run_umap_inplace run_from_json(params_with_layer, save_results=False) @@ -152,9 +163,12 @@ def test_default_parameters(self) -> None: with patch('spac.templates.umap_transformation_template.' 'run_umap') as mock_umap: - mock_adata_obj = mock_adata() - mock_adata_obj.obsm["X_umap"] = np.random.rand(10, 2) - mock_umap.return_value = mock_adata_obj + # Mock run_umap to modify in-place + def mock_run_umap_inplace(adata, **kwargs): + adata.obsm["X_umap"] = np.random.rand(adata.n_obs, 2) + return adata + + mock_umap.side_effect = mock_run_umap_inplace run_from_json(minimal_params, save_results=False) From ca2933068c63c0a877970a496d4d9e2afe34d447 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 20:35:56 -0400 Subject: [PATCH 045/102] feat(phenograph_clustering_template): add phenograph_clustering_template function and unit tests --- .../phenograph_clustering_template.py | 138 ++++++++++++++++ .../test_phenograph_clustering_template.py | 149 ++++++++++++++++++ 2 files changed, 287 insertions(+) create mode 100644 src/spac/templates/phenograph_clustering_template.py create mode 100644 tests/templates/test_phenograph_clustering_template.py diff --git a/src/spac/templates/phenograph_clustering_template.py b/src/spac/templates/phenograph_clustering_template.py new file mode 100644 index 00000000..820a768a --- /dev/null +++ b/src/spac/templates/phenograph_clustering_template.py @@ -0,0 +1,138 @@ +""" +Platform-agnostic Phenograph Clustering template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.phenograph_clustering_template import run_from_json +>>> run_from_json("examples/phenograph_clustering_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union +import pandas as pd + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.transformations import phenograph_clustering +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], Any]: + """ + Execute Phenograph Clustering analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the adata object + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or AnnData + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed AnnData object + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + Layer_name = params.get("Table_to_Process", "Original") + K_cluster = params.get("K_Nearest_Neighbors", 30) + Seed = params.get("Seed", 42) + resolution_parameter = params.get("Resolution_Parameter", 1.0) + output_annotation_name = params.get( + "Output_Annotation_Name", "phenograph" + ) + # Used only in HPC profiling mode (not implemented in SPAC) + resolution_list = params.get("Resolution_List", []) + + n_iterations = params.get("Number_of_Iterations", 100) + + if Layer_name == "Original": + Layer_name = None + + intensities = adata.var.index.to_list() + + print("Before Phenograph Clustering: \n", adata) + + phenograph_clustering( + adata=adata, + features=intensities, + layer=Layer_name, + k=K_cluster, + seed=Seed, + resolution_parameter=resolution_parameter, + n_iterations=n_iterations + ) + if output_annotation_name != "phenograph": + adata.obs = adata.obs.rename( + columns={'phenograph': output_annotation_name} + ) + + print("After Phenograph Clustering: \n", adata) + + # Count and display occurrences of each label in the annotation + print( + f'Count of cells in the output annotation:' + f'"{output_annotation_name}":' + ) + label_counts = adata.obs[output_annotation_name].value_counts() + print(label_counts) + print("\n") + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "transform_output.pickle") + # Default to pickle format if no recognized extension + if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): + output_file = output_file + '.pickle' + + saved_files = save_outputs({output_file: adata}) + + print( + f"Phenograph Clustering completed → " + f"{saved_files[output_file]}" + ) + return saved_files + else: + # Return the adata object directly for in-memory workflows + print("Returning AnnData object (not saving to file)") + return adata + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python phenograph_clustering_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned AnnData object") \ No newline at end of file diff --git a/tests/templates/test_phenograph_clustering_template.py b/tests/templates/test_phenograph_clustering_template.py new file mode 100644 index 00000000..14e485b5 --- /dev/null +++ b/tests/templates/test_phenograph_clustering_template.py @@ -0,0 +1,149 @@ +# tests/templates/test_phenograph_clustering_template.py +"""Unit tests for the Phenograph Clustering template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.phenograph_clustering_template import run_from_json + + +def mock_adata(n_cells: int = 100) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], + "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] + }) + # Create expression matrix with some structure for clustering + x_mat = rng.normal(size=(n_cells, 10)) + # Add some signal to make clustering meaningful + # First half of cells higher in first 5 genes + x_mat[:n_cells//2, :5] += 2.0 + # Second half higher in last 5 genes + x_mat[n_cells//2:, 5:] += 2.0 + + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = [f"Gene{i}" for i in range(10)] + adata.var.index.name = None # Match NIDAP style + return adata + + +class TestPhenographClusteringTemplate(unittest.TestCase): + """Unit tests for the Phenograph Clustering template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "output" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters - using proper Python types for defaults + self.params = { + "Upstream_Analysis": self.in_file, + "Table_to_Process": "Original", + "K_Nearest_Neighbors": 30, + "Seed": 42, + "Resolution_Parameter": 1.0, + "Output_Annotation_Name": "phenograph", + "Resolution_List": [], + "Number_of_Iterations": 100, + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + # Verify file was saved + self.assertTrue(len(result) > 0) + output_path = list(result.values())[0] + self.assertTrue(os.path.exists(output_path)) + + # Load and verify the output + with open(output_path, 'rb') as f: + adata_out = pickle.load(f) + self.assertIn("phenograph", adata_out.obs.columns) + + # Test 2: Run without saving + result_no_save = run_from_json(self.params, save_results=False) + # Check appropriate return type based on template + self.assertIsInstance(result_no_save, ad.AnnData) + self.assertIn("phenograph", result_no_save.obs.columns) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + def test_function_calls(self) -> None: + """Test that main function is called with correct parameters.""" + with patch('spac.templates.phenograph_clustering_template.' + 'phenograph_clustering') as mock_pheno: + # Mock the main function + def mock_clustering(adata, **kwargs): + # Add the phenograph column + n_half = len(adata) // 2 + adata.obs['phenograph'] = pd.Categorical( + ['0'] * n_half + ['1'] * (len(adata) - n_half) + ) + return None + + mock_pheno.side_effect = mock_clustering + + # Test with custom annotation name + params_custom = self.params.copy() + params_custom["Output_Annotation_Name"] = "my_clusters" + params_custom["K_Nearest_Neighbors"] = 50 + params_custom["Resolution_Parameter"] = 0.5 + + result = run_from_json(params_custom, save_results=False) + + # Verify function was called correctly + mock_pheno.assert_called_once() + call_args = mock_pheno.call_args + + # Check specific parameter conversions + self.assertEqual(call_args[1]['k'], 50) + self.assertEqual(call_args[1]['resolution_parameter'], 0.5) + self.assertEqual(call_args[1]['seed'], 42) + self.assertEqual(call_args[1]['n_iterations'], 100) + # "Original" -> None + self.assertEqual(call_args[1]['layer'], None) + + # Check that phenograph was renamed + self.assertIn("my_clusters", result.obs.columns) + self.assertNotIn("phenograph", result.obs.columns) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From d67f6c7278e7c8622ca43832acf8b74ae4a4363e Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 21:18:20 -0400 Subject: [PATCH 046/102] feat(umap_tsne_pca_template): add umap_tsne_pca_template function and unit tests --- src/spac/templates/umap_tsne_pca_template.py | 188 ++++++++++++++++++ .../templates/test_umap_tsne_pca_template.py | 181 +++++++++++++++++ 2 files changed, 369 insertions(+) create mode 100644 src/spac/templates/umap_tsne_pca_template.py create mode 100644 tests/templates/test_umap_tsne_pca_template.py diff --git a/src/spac/templates/umap_tsne_pca_template.py b/src/spac/templates/umap_tsne_pca_template.py new file mode 100644 index 00000000..5dd27227 --- /dev/null +++ b/src/spac/templates/umap_tsne_pca_template.py @@ -0,0 +1,188 @@ +""" +Platform-agnostic UMAP\tSNE\PCA Visualization template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.umap_tsne_pca_template import run_from_json +>>> run_from_json("examples/umap_tsne_pca_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, Optional, Tuple +import pandas as pd +import matplotlib.pyplot as plt + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.visualization import dimensionality_reduction_plot +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True, + show_plot: bool = True +) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: + """ + Execute UMAP\tSNE\PCA Visualization analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the figure and + dataframe directly for in-memory workflows. Default is True. + show_plot : bool, optional + Whether to display the plot. Default is True. + + Returns + ------- + dict or tuple + If save_results=True: Dictionary of saved file paths + If save_results=False: Tuple of (figure, dataframe) + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + annotation = params.get("Annotation_to_Highlight", "None") + feature = params.get("Feature_to_Highlight", "None") + layer = params.get("Table", "Original") + method = params.get("Dimension_Reduction_Method", "umap") + fig_width = params.get("Figure_Width", 12) + fig_height = params.get("Figure_Height", 12) + font_size = params.get("Font_Size", 12) + fig_dpi = params.get("Figure_DPI", 300) + legend_location = params.get("Legend_Location", "best") + legend_label_size = params.get("Legend_Font_Size", 16) + legend_marker_scale = params.get("Legend_Marker_Size", 5.0) + color_by = params.get("Color_By", "Annotation") + point_size = params.get("Dot_Size", 1) + v_min = params.get("Value_Min", "None") + v_max = params.get("Value_Max", "None") + + feature = text_to_value(feature) + annotation = text_to_value(annotation) + + if color_by == "Annotation": + feature = None + else: + annotation = None + + # Store the original value of layer + layer_input = layer + + layer = text_to_value(layer, default_none_text="Original") + + vmin = text_to_value( + v_min, + default_none_text="None", + value_to_convert_to=None, + to_float=True, + param_name="Value Min" + ) + + vmax = text_to_value( + v_max, + default_none_text="None", + value_to_convert_to=None, + to_float=True, + param_name="Value Max" + ) + + plt.rcParams.update({'font.size': font_size}) + + fig, ax = dimensionality_reduction_plot( + adata=adata, + method=method, + annotation=annotation, + feature=feature, + layer=layer, + point_size=point_size, + vmin=vmin, + vmax=vmax + ) + + if color_by == "Annotation": + title = annotation + else: + title = f'Table:"{layer_input}" \n Feature:"{feature}"' + ax.set_title(title) + + fig = ax.get_figure() + + fig.set_size_inches( + fig_width, + fig_height + ) + fig.set_dpi(fig_dpi) + + legend = ax.get_legend() + has_legend = legend is not None + + if has_legend: + ax.legend( + loc=legend_location, + bbox_to_anchor=(1, 0.5), + fontsize=legend_label_size, + markerscale=legend_marker_scale + ) + + plt.tight_layout() + + if show_plot: + plt.show() + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "plots.png") + # Ensure proper extension + if not output_file.endswith(('.png', '.pdf', '.svg')): + output_file = output_file + '.png' + + fig.savefig(output_file, dpi=fig_dpi, bbox_inches='tight') + saved_files = {output_file: output_file} + + print( + f"UMAP\\tSNE\\PCA Visualization completed → " + f"{saved_files[output_file]}" + ) + return saved_files + else: + # Return the figure and dataframe directly for in-memory workflows + print("Returning figure (not saving to file)") + # Note: This template doesn't produce a dataframe, just a figure + return fig, None + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python umap_tsne_pca_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned figure") \ No newline at end of file diff --git a/tests/templates/test_umap_tsne_pca_template.py b/tests/templates/test_umap_tsne_pca_template.py new file mode 100644 index 00000000..faf2bdd3 --- /dev/null +++ b/tests/templates/test_umap_tsne_pca_template.py @@ -0,0 +1,181 @@ +# tests/templates/test_umap_tsne_pca_template.py +"""Unit tests for the UMAP\tSNE\PCA Visualization template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import matplotlib +matplotlib.use("Agg") # Headless backend for CI + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.umap_tsne_pca_template import run_from_json + + +def mock_adata(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], + "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] + }) + x_mat = rng.normal(size=(n_cells, 3)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = ["Gene1", "Gene2", "Gene3"] + # Add UMAP coordinates for visualization + adata.obsm["X_umap"] = rng.random((n_cells, 2)) * 10 + adata.obsm["X_tsne"] = rng.random((n_cells, 2)) * 50 + adata.obsm["X_pca"] = rng.random((n_cells, 2)) * 5 + return adata + + +class TestUmapTsnePcaTemplate(unittest.TestCase): + """Unit tests for the UMAP\tSNE\PCA Visualization template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "visualization" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters - adjust based on template + self.params = { + "Upstream_Analysis": self.in_file, + "Annotation_to_Highlight": "cell_type", + "Feature_to_Highlight": "None", + "Table": "Original", + "Dimension_Reduction_Method": "umap", + "Figure_Width": 12, + "Figure_Height": 12, + "Font_Size": 12, + "Figure_DPI": 300, + "Legend_Location": "best", + "Legend_Font_Size": 16, + "Legend_Marker_Size": 5.0, + "Color_By": "Annotation", + "Dot_Size": 1, + "Value_Min": "None", + "Value_Max": "None", + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Mock the dimensionality_reduction_plot function + with patch('spac.templates.umap_tsne_pca_template.' + 'dimensionality_reduction_plot') as mock_plot: + # Create mock figure and axis + mock_fig = MagicMock() + mock_ax = MagicMock() + mock_ax.get_figure.return_value = mock_fig + mock_ax.get_legend.return_value = MagicMock() + mock_fig.savefig = MagicMock() + + mock_plot.return_value = (mock_fig, mock_ax) + + # Test 1: Run with default parameters + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + # Check that output file has proper extension + output_files = list(result.keys()) + self.assertTrue( + any(f.endswith('.png') for f in output_files) + ) + + # Test 2: Run without saving + result_no_save = run_from_json( + self.params, save_results=False + ) + # Check appropriate return type based on template + self.assertIsInstance(result_no_save, tuple) + fig, df = result_no_save + self.assertEqual(fig, mock_fig) + # This template doesn't return a dataframe + self.assertIsNone(df) + + # Test 3: JSON file input + json_path = os.path.join( + self.tmp_dir.name, "params.json" + ) + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test invalid float conversion for vmin + params_bad = self.params.copy() + params_bad["Value_Min"] = "invalid_number" + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + # Check exact error message + expected_msg = ( + "Error: can't convert Value Min to float. " + "Received:\"invalid_number\"" + ) + self.assertEqual(str(context.exception), expected_msg) + + @patch('spac.templates.umap_tsne_pca_template.' + 'dimensionality_reduction_plot') + def test_function_calls(self, mock_plot) -> None: + """Test that main function is called with correct parameters.""" + # Mock the main function + mock_fig = MagicMock() + mock_ax = MagicMock() + mock_ax.get_figure.return_value = mock_fig + mock_ax.get_legend.return_value = None + mock_plot.return_value = (mock_fig, mock_ax) + + # Test with feature coloring instead of annotation + params_feature = self.params.copy() + params_feature["Color_By"] = "Feature" + params_feature["Feature_to_Highlight"] = "Gene1" + params_feature["Annotation_to_Highlight"] = "None" + params_feature["Value_Min"] = "0.5" + params_feature["Value_Max"] = "10.0" + + run_from_json(params_feature, save_results=False, show_plot=False) + + # Verify function was called correctly + mock_plot.assert_called_once() + call_args = mock_plot.call_args + + # Check that annotation is None and feature is set + self.assertIsNone(call_args[1]['annotation']) + self.assertEqual(call_args[1]['feature'], "Gene1") + self.assertEqual(call_args[1]['vmin'], 0.5) + self.assertEqual(call_args[1]['vmax'], 10.0) + self.assertEqual(call_args[1]['method'], "umap") + self.assertEqual(call_args[1]['point_size'], 1) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 7fb9b2cd291e3aa3651e5b3d01240772330609ff Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 21:27:11 -0400 Subject: [PATCH 047/102] fix(umap_tsne_pca_template): address the comments from copilot --- src/spac/templates/umap_tsne_pca_template.py | 4 ++-- tests/templates/test_umap_tsne_pca_template.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spac/templates/umap_tsne_pca_template.py b/src/spac/templates/umap_tsne_pca_template.py index 5dd27227..49fac9f7 100644 --- a/src/spac/templates/umap_tsne_pca_template.py +++ b/src/spac/templates/umap_tsne_pca_template.py @@ -1,5 +1,5 @@ """ -Platform-agnostic UMAP\tSNE\PCA Visualization template converted from NIDAP. +Platform-agnostic UMAP\\tSNE\\PCA Visualization template converted from NIDAP. Maintains the exact logic from the NIDAP template. Usage @@ -32,7 +32,7 @@ def run_from_json( show_plot: bool = True ) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: """ - Execute UMAP\tSNE\PCA Visualization analysis with parameters from JSON. + Execute UMAP\\tSNE\\PCA Visualization analysis with parameters from JSON. Replicates the NIDAP template functionality exactly. Parameters diff --git a/tests/templates/test_umap_tsne_pca_template.py b/tests/templates/test_umap_tsne_pca_template.py index faf2bdd3..e1d9bd18 100644 --- a/tests/templates/test_umap_tsne_pca_template.py +++ b/tests/templates/test_umap_tsne_pca_template.py @@ -1,5 +1,5 @@ # tests/templates/test_umap_tsne_pca_template.py -"""Unit tests for the UMAP\tSNE\PCA Visualization template.""" +"""Unit tests for the UMAP\\tSNE\\PCA Visualization template.""" import json import os @@ -43,7 +43,7 @@ def mock_adata(n_cells: int = 10) -> ad.AnnData: class TestUmapTsnePcaTemplate(unittest.TestCase): - """Unit tests for the UMAP\tSNE\PCA Visualization template.""" + """Unit tests for the UMAP\\tSNE\\PCA Visualization template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() From 96446d70e392aa00e1ba77b2abddaa040d6aea7a Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 22:04:00 -0400 Subject: [PATCH 048/102] feat(rename_labels_template): add rename_labels_template function and unit tests --- src/spac/templates/rename_labels_template.py | 126 +++++++++++++ .../templates/test_rename_labels_template.py | 168 ++++++++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 src/spac/templates/rename_labels_template.py create mode 100644 tests/templates/test_rename_labels_template.py diff --git a/src/spac/templates/rename_labels_template.py b/src/spac/templates/rename_labels_template.py new file mode 100644 index 00000000..6be49f78 --- /dev/null +++ b/src/spac/templates/rename_labels_template.py @@ -0,0 +1,126 @@ +""" +Platform-agnostic Rename Labels template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.rename_labels_template import run_from_json +>>> run_from_json("examples/rename_labels_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union +import pandas as pd +import pickle + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.transformations import rename_annotations +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], Any]: + """ + Execute Rename Labels analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the adata object + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or AnnData + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed AnnData object + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + all_data = load_input(params["Upstream_Analysis"]) + + # Extract parameters + rename_list_path = params["Cluster_Mapping_Dictionary"] + original_column = params.get("Source_Annotation", "None") + renamed_column = params.get("New_Annotation", "None") + + # Load the mapping dictionary CSV + rename_list = pd.read_csv(rename_list_path) + + original_column = text_to_value(original_column) + renamed_column = text_to_value(renamed_column) + + # Create a new dictionary with the desired format + + dict_list = rename_list.to_dict('records') + + mappings = {d['Original']: d['New'] for d in dict_list} + + print("Cluster Name Mapping: \n", mappings) + + rename_annotations( + all_data, + src_annotation=original_column, + dest_annotation=renamed_column, + mappings=mappings) + + print("After Renaming Clusters: \n", all_data) + + # Count and display occurrences of each label in the annotation + print(f'Count of cells in the output annotation:"{renamed_column}":') + label_counts = all_data.obs[renamed_column].value_counts() + print(label_counts) + print("\n") + + object_to_output = all_data + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "transform_output.pickle") + # Default to pickle format if no recognized extension + if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): + output_file = output_file + '.pickle' + + saved_files = save_outputs({output_file: object_to_output}) + + print(f"Rename Labels completed → {saved_files[output_file]}") + return saved_files + else: + # Return the adata object directly for in-memory workflows + print("Returning AnnData object (not saving to file)") + return object_to_output + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python rename_labels_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned AnnData object") \ No newline at end of file diff --git a/tests/templates/test_rename_labels_template.py b/tests/templates/test_rename_labels_template.py new file mode 100644 index 00000000..7d9d1e7d --- /dev/null +++ b/tests/templates/test_rename_labels_template.py @@ -0,0 +1,168 @@ +# tests/templates/test_rename_labels_template.py +"""Unit tests for the Rename Labels template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.rename_labels_template import run_from_json + + +def mock_adata(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "phenograph_k60_r1": [str(i % 3) for i in range(n_cells)], + "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] + }) + x_mat = rng.normal(size=(n_cells, 3)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = ["Gene1", "Gene2", "Gene3"] + return adata + + +class TestRenameLabelsTemplate(unittest.TestCase): + """Unit tests for the Rename Labels template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.mapping_file = os.path.join( + self.tmp_dir.name, "mapping.csv" + ) + self.out_file = "output" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Create mapping CSV - pandas will read these as integers + mapping_df = pd.DataFrame({ + 'Original': [0, 1, 2], + 'New': ['TypeA', 'TypeB', 'TypeC'] + }) + mapping_df.to_csv(self.mapping_file, index=False) + + # Minimal parameters from NIDAP template + self.params = { + "Upstream_Analysis": self.in_file, + "Cluster_Mapping_Dictionary": self.mapping_file, + "Source_Annotation": "phenograph_k60_r1", + "New_Annotation": "renamed_phenotypes", + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + # Verify file was saved + self.assertTrue(len(result) > 0) + + # Test 2: Run without saving + result_no_save = run_from_json(self.params, save_results=False) + # Check appropriate return type based on template + self.assertIsInstance(result_no_save, ad.AnnData) + # Verify new annotation exists + self.assertIn("renamed_phenotypes", result_no_save.obs.columns) + # Verify mapping was applied + unique_labels = result_no_save.obs["renamed_phenotypes"].unique() + self.assertIn("TypeA", unique_labels) + self.assertIn("TypeB", unique_labels) + self.assertIn("TypeC", unique_labels) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test missing required mapping columns + bad_mapping_df = pd.DataFrame({ + 'Wrong': ['0', '1', '2'], + 'Columns': ['TypeA', 'TypeB', 'TypeC'] + }) + bad_mapping_file = os.path.join( + self.tmp_dir.name, "bad_mapping.csv" + ) + bad_mapping_df.to_csv(bad_mapping_file, index=False) + + params_bad = self.params.copy() + params_bad["Cluster_Mapping_Dictionary"] = bad_mapping_file + + with self.assertRaises(KeyError) as context: + run_from_json(params_bad) + + # Check that error occurs when accessing 'Original' column + self.assertIn("Original", str(context.exception)) + + @patch('spac.templates.rename_labels_template.rename_annotations') + def test_function_calls(self, mock_rename) -> None: + """Test that main function is called with correct parameters.""" + # Mock the rename_annotations function to simulate its effect + def side_effect_rename(adata, src_annotation, dest_annotation, + mappings): + # Simulate what rename_annotations does + # Convert string values to int if mapping keys are int + if all(isinstance(k, int) for k in mappings.keys()): + adata.obs[dest_annotation] = ( + adata.obs[src_annotation].astype(int).map(mappings) + ) + else: + adata.obs[dest_annotation] = ( + adata.obs[src_annotation].map(mappings) + ) + return None + + mock_rename.side_effect = side_effect_rename + + # Run the template + result = run_from_json(self.params, save_results=False) + + # Verify function was called correctly + mock_rename.assert_called_once() + call_args = mock_rename.call_args + + # Check that AnnData was passed + self.assertIsInstance(call_args[0][0], ad.AnnData) + + # Check keyword arguments + self.assertEqual( + call_args[1]['src_annotation'], "phenograph_k60_r1" + ) + self.assertEqual( + call_args[1]['dest_annotation'], "renamed_phenotypes" + ) + expected_mappings = {0: 'TypeA', 1: 'TypeB', 2: 'TypeC'} + self.assertEqual(call_args[1]['mappings'], expected_mappings) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From c57075d87995b7f35b0c9065a354363f7cceb623 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 23:10:15 -0400 Subject: [PATCH 049/102] feat(relational_heatmap_template): add relational_heatmap_template function and unit tests --- .../templates/relational_heatmap_template.py | 166 ++++++++++++++ .../test_relational_heatmap_template.py | 214 ++++++++++++++++++ 2 files changed, 380 insertions(+) create mode 100644 src/spac/templates/relational_heatmap_template.py create mode 100644 tests/templates/test_relational_heatmap_template.py diff --git a/src/spac/templates/relational_heatmap_template.py b/src/spac/templates/relational_heatmap_template.py new file mode 100644 index 00000000..f9cd846b --- /dev/null +++ b/src/spac/templates/relational_heatmap_template.py @@ -0,0 +1,166 @@ +""" +Platform-agnostic Relational Heatmap template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.relational_heatmap_template import run_from_json +>>> run_from_json("examples/relational_heatmap_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, Optional, Tuple +import pandas as pd +import plotly.io as pio +import matplotlib.pyplot as plt + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.visualization import relational_heatmap +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: + """ + Execute Relational Heatmap analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the figure and + dataframe directly for in-memory workflows. Default is True. + + Returns + ------- + dict or tuple + If save_results=True: Dictionary of saved file paths + If save_results=False: Tuple of (figure, dataframe) + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + annotation_columns = [ + params.get("Source_Annotation_Name", "None"), + params.get("Target_Annotation_Name", "None") + ] + dpi = params.get("Figure_DPI", 300) + width_in = params.get("Figure_Width_inch", 8) + height_in = params.get("Figure_Height_inch", 10) + width_px = width_in * 96 + print(width_px) + height_px = height_in * 96 + print(height_px) + + scale = dpi / 96 + + font_size = params.get("Font_Size", 8) + colormap = params.get("Colormap", "darkmint") + + source_annotation = text_to_value(annotation_columns[0]) + + target_annotation = text_to_value(annotation_columns[1]) + + result_dict = relational_heatmap( + adata=adata, + source_annotation=source_annotation, + target_annotation=target_annotation, + color_map=colormap, + font_size=font_size + ) + + # extract results from function return + rhmap_file_name = result_dict['file_name'] + rhmap_data = result_dict['data'] + fig = result_dict['figure'] + + # Generate temporary image name for Plotly export + import tempfile + tmp_image_name = tempfile.mktemp(suffix='.png') + + pio.write_image( + fig, + tmp_image_name, + width=width_px, # Specify the width in pixels + height=height_px, + engine='kaleido', # Use the 'kaleido' engine for high DPI images + scale=scale + ) + + img = plt.imread(tmp_image_name) + static, axs = plt.subplots( + 1, 1, figsize=(width_in, height_in), dpi=dpi + ) + + # Load and display the image using Matplotlib + axs.imshow(img) + axs.axis('off') + + # Display the matplotlib static figure + plt.show() + + # Clean up temp file + import os + if os.path.exists(tmp_image_name): + os.remove(tmp_image_name) + + # Display the Plotly interactive figure + fig.show() + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", rhmap_file_name) + if not output_file.endswith('.csv'): + output_file = output_file + '.csv' + + saved_files = save_outputs({output_file: rhmap_data}) + + # Also save the static plot + plot_file = output_file.replace('.csv', '.png') + static.savefig(plot_file, dpi=dpi, bbox_inches='tight') + saved_files[plot_file] = plot_file + + print( + f"Relational Heatmap completed → {list(saved_files.keys())}" + ) + return saved_files + else: + # Return the figure and dataframe directly for in-memory workflows + print("Returning figure and dataframe (not saving to file)") + return static, rhmap_data + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python relational_heatmap_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned figure and dataframe") \ No newline at end of file diff --git a/tests/templates/test_relational_heatmap_template.py b/tests/templates/test_relational_heatmap_template.py new file mode 100644 index 00000000..676810fa --- /dev/null +++ b/tests/templates/test_relational_heatmap_template.py @@ -0,0 +1,214 @@ +# tests/templates/test_relational_heatmap_template.py +"""Unit tests for the Relational Heatmap template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock, Mock + +import matplotlib +import matplotlib.pyplot as plt +matplotlib.use("Agg") # Headless backend for CI + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.relational_heatmap_template import run_from_json + + +def mock_adata(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "phenograph_k60_r1": (["cluster1", "cluster2", "cluster3"] * + ((n_cells + 2) // 3))[:n_cells], + "renamed_phenotypes": (["phenotype_A", "phenotype_B"] * + ((n_cells + 1) // 2))[:n_cells] + }) + x_mat = rng.normal(size=(n_cells, 3)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = ["Gene1", "Gene2", "Gene3"] + return adata + + +class TestRelationalHeatmapTemplate(unittest.TestCase): + """Unit tests for the Relational Heatmap template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "relational_heatmap" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters from NIDAP template + self.params = { + "Upstream_Analysis": self.in_file, + "Source_Annotation_Name": "phenograph_k60_r1", + "Target_Annotation_Name": "renamed_phenotypes", + "Colormap": "darkmint", + "Figure_Width_inch": 8, + "Figure_Height_inch": 10, + "Figure_DPI": 300, + "Font_Size": 8, + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + @patch('spac.templates.relational_heatmap_template.relational_heatmap') + @patch('plotly.io.write_image') + @patch('matplotlib.pyplot.show') # Mock plt.show() + def test_complete_io_workflow( + self, mock_plt_show, mock_write_image, mock_relational + ) -> None: + """Single I/O test covering input/output scenarios.""" + # Mock the relational_heatmap function + mock_fig = Mock() + mock_fig.show = Mock() # Mock the fig.show() method + + mock_df = pd.DataFrame({ + 'source': ['cluster1', 'cluster2'], + 'target': ['phenotype_A', 'phenotype_B'], + 'value': [5, 3] + }) + + mock_relational.return_value = { + 'file_name': 'relational_heatmap.csv', + 'data': mock_df, + 'figure': mock_fig + } + + # Mock the plotly write_image to create a dummy image + def create_dummy_image(fig, path, **kwargs): + # Create a minimal PNG file + fig, ax = plt.subplots(figsize=(1, 1)) + ax.text(0.5, 0.5, 'test', ha='center', va='center') + plt.savefig(path, dpi=72) + plt.close(fig) + + mock_write_image.side_effect = create_dummy_image + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + # Should have both CSV and PNG files + self.assertEqual(len(result), 2) + csv_files = [f for f in result.keys() if f.endswith('.csv')] + png_files = [f for f in result.keys() if f.endswith('.png')] + self.assertEqual(len(csv_files), 1) + self.assertEqual(len(png_files), 1) + + # Test 2: Run without saving + result_no_save = run_from_json( + self.params, save_results=False + ) + # Check appropriate return type - should be (figure, dataframe) + self.assertIsInstance(result_no_save, tuple) + self.assertEqual(len(result_no_save), 2) + fig, df = result_no_save + self.assertIsInstance(df, pd.DataFrame) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + @patch('matplotlib.pyplot.show') # Mock plt.show() + def test_error_validation(self, mock_plt_show) -> None: + """Test exact error message for invalid parameters.""" + # Test with None annotations (should be handled by text_to_value) + params_none = self.params.copy() + params_none["Source_Annotation_Name"] = "None" + params_none["Target_Annotation_Name"] = "None" + + with patch('spac.templates.relational_heatmap_template.' + 'relational_heatmap') as mock_rel: + # The template should pass None values to the function + mock_rel.return_value = { + 'file_name': 'test.csv', + 'data': pd.DataFrame(), + 'figure': Mock(show=Mock()) # Mock fig.show() + } + + # Mock write_image to create a dummy file + def create_dummy_image(fig, path, **kwargs): + # Create a minimal PNG file + fig, ax = plt.subplots(figsize=(1, 1)) + ax.text(0.5, 0.5, 'test', ha='center', va='center') + plt.savefig(path, dpi=72) + plt.close(fig) + + with patch('plotly.io.write_image', + side_effect=create_dummy_image): + run_from_json(params_none) + + # Verify None was passed + call_args = mock_rel.call_args + self.assertIsNone(call_args[1]['source_annotation']) + self.assertIsNone(call_args[1]['target_annotation']) + + @patch('spac.templates.relational_heatmap_template.relational_heatmap') + @patch('plotly.io.write_image') + @patch('matplotlib.pyplot.show') # Mock plt.show() + def test_function_calls( + self, mock_plt_show, mock_write_image, mock_relational + ) -> None: + """Test that main function is called with correct parameters.""" + # Mock the main function + mock_relational.return_value = { + 'file_name': 'test.csv', + 'data': pd.DataFrame({'a': [1, 2]}), + 'figure': Mock(show=Mock()) # Mock fig.show() + } + + # Mock write_image to create a dummy file + def create_dummy_image(fig, path, **kwargs): + # Create a minimal PNG file + fig, ax = plt.subplots(figsize=(1, 1)) + ax.text(0.5, 0.5, 'test', ha='center', va='center') + plt.savefig(path, dpi=72) + plt.close(fig) + + mock_write_image.side_effect = create_dummy_image + + run_from_json(self.params, save_results=False) + + # Verify function was called correctly + mock_relational.assert_called_once() + call_args = mock_relational.call_args + + # Check specific parameters + self.assertEqual( + call_args[1]['source_annotation'], 'phenograph_k60_r1' + ) + self.assertEqual( + call_args[1]['target_annotation'], 'renamed_phenotypes' + ) + self.assertEqual(call_args[1]['color_map'], 'darkmint') + self.assertEqual(call_args[1]['font_size'], 8) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 5662bdbf77980af891bd55e45c02a20ed7af7546 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sat, 2 Aug 2025 23:52:03 -0400 Subject: [PATCH 050/102] fix(relational_heatmap_template): address the issue of insecure temporary file and comments from copilot --- .../templates/relational_heatmap_template.py | 7 ++++-- .../test_relational_heatmap_template.py | 24 ++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/spac/templates/relational_heatmap_template.py b/src/spac/templates/relational_heatmap_template.py index f9cd846b..3cd403ea 100644 --- a/src/spac/templates/relational_heatmap_template.py +++ b/src/spac/templates/relational_heatmap_template.py @@ -9,6 +9,7 @@ """ import json import sys +import os from pathlib import Path from typing import Any, Dict, Union, Optional, Tuple import pandas as pd @@ -92,7 +93,10 @@ def run_from_json( # Generate temporary image name for Plotly export import tempfile - tmp_image_name = tempfile.mktemp(suffix='.png') + with tempfile.NamedTemporaryFile( + delete=False, suffix='.png' + ) as tmp_file: + tmp_image_name = tmp_file.name pio.write_image( fig, @@ -116,7 +120,6 @@ def run_from_json( plt.show() # Clean up temp file - import os if os.path.exists(tmp_image_name): os.remove(tmp_image_name) diff --git a/tests/templates/test_relational_heatmap_template.py b/tests/templates/test_relational_heatmap_template.py index 676810fa..2620656a 100644 --- a/tests/templates/test_relational_heatmap_template.py +++ b/tests/templates/test_relational_heatmap_template.py @@ -97,10 +97,14 @@ def test_complete_io_workflow( # Mock the plotly write_image to create a dummy image def create_dummy_image(fig, path, **kwargs): # Create a minimal PNG file - fig, ax = plt.subplots(figsize=(1, 1)) + # Ensure file path exists (for NamedTemporaryFile) + if not os.path.exists(path): + # Create parent directory if needed + os.makedirs(os.path.dirname(path), exist_ok=True) + fig_dummy, ax = plt.subplots(figsize=(1, 1)) ax.text(0.5, 0.5, 'test', ha='center', va='center') plt.savefig(path, dpi=72) - plt.close(fig) + plt.close(fig_dummy) mock_write_image.side_effect = create_dummy_image @@ -155,10 +159,14 @@ def test_error_validation(self, mock_plt_show) -> None: # Mock write_image to create a dummy file def create_dummy_image(fig, path, **kwargs): # Create a minimal PNG file - fig, ax = plt.subplots(figsize=(1, 1)) + # Ensure file path exists (for NamedTemporaryFile) + if not os.path.exists(path): + # Create parent directory if needed + os.makedirs(os.path.dirname(path), exist_ok=True) + fig_dummy, ax = plt.subplots(figsize=(1, 1)) ax.text(0.5, 0.5, 'test', ha='center', va='center') plt.savefig(path, dpi=72) - plt.close(fig) + plt.close(fig_dummy) with patch('plotly.io.write_image', side_effect=create_dummy_image): @@ -186,10 +194,14 @@ def test_function_calls( # Mock write_image to create a dummy file def create_dummy_image(fig, path, **kwargs): # Create a minimal PNG file - fig, ax = plt.subplots(figsize=(1, 1)) + # Ensure file path exists (for NamedTemporaryFile) + if not os.path.exists(path): + # Create parent directory if needed + os.makedirs(os.path.dirname(path), exist_ok=True) + fig_dummy, ax = plt.subplots(figsize=(1, 1)) ax.text(0.5, 0.5, 'test', ha='center', va='center') plt.savefig(path, dpi=72) - plt.close(fig) + plt.close(fig_dummy) mock_write_image.side_effect = create_dummy_image From 34b4eee45ae98fd0ce7c5f184611f4993125949f Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sun, 3 Aug 2025 00:12:27 -0400 Subject: [PATCH 051/102] feat(sankey_plot_template): add sankey_plot_template function and unit tests --- src/spac/templates/sankey_plot_template.py | 167 ++++++++++++++++ tests/templates/test_sankey_plot_template.py | 199 +++++++++++++++++++ 2 files changed, 366 insertions(+) create mode 100644 src/spac/templates/sankey_plot_template.py create mode 100644 tests/templates/test_sankey_plot_template.py diff --git a/src/spac/templates/sankey_plot_template.py b/src/spac/templates/sankey_plot_template.py new file mode 100644 index 00000000..5e544216 --- /dev/null +++ b/src/spac/templates/sankey_plot_template.py @@ -0,0 +1,167 @@ +""" +Platform-agnostic Sankey Plot template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.sankey_plot_template import run_from_json +>>> run_from_json("examples/sankey_plot_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, Optional, Tuple +import pandas as pd +import matplotlib.pyplot as plt +import plotly.io as pio + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.visualization import sankey_plot +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True, + show_plot: bool = True +) -> Union[Dict[str, str], None]: + """ + Execute Sankey Plot analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns None + since this template creates multiple plot files. Default is True. + show_plot : bool, optional + Whether to display the plot. Default is True. + + Returns + ------- + dict or None + If save_results=True: Dictionary of saved file paths + If save_results=False: None (plots are displayed but not saved) + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + annotation_columns = [ + params.get("Source_Annotation_Name", "None"), + params.get("Target_Annotation_Name", "None") + ] + dpi = params.get("Figure_DPI", 300) + width_num = params.get("Figure_Width_inch", 6) + scale = dpi / 96 + width_in_pixels = width_num / scale * dpi + + height_num = params.get("Figure_Height_inch", 6) + height_in_pixels = height_num / scale * dpi + + sort_asscend = True + source_color_map = params.get("Source_Annotation_Color_Map", "tab20") + target_color_map = params.get("Target_Annotation_Color_Map", "tab20b") + + sankey_font = params.get("Font_Size", 12) + + target_annotation = text_to_value(annotation_columns[1]) + source_annotation = text_to_value(annotation_columns[0]) + + fig = sankey_plot( + adata=adata, + source_annotation=source_annotation, + target_annotation=target_annotation, + source_color_map=source_color_map, + target_color_map=target_color_map, + sankey_font=sankey_font + ) + + # Customize the Sankey diagram layout + fig.update_layout( + width=width_in_pixels, # Specify the width in pixels + height=height_in_pixels # Specify the height in pixels + ) + + # Show the plot with the specified display options + print(fig) + + image_path = 'sankey_diagram.png' + + pio.write_image( + fig, + image_path, + width=width_in_pixels, # Specify the width in pixels + height=height_in_pixels, + engine='kaleido', # Use the 'kaleido' engine for high DPI images + scale=scale + ) + + img = plt.imread(image_path) + static, axs = plt.subplots(1, 1, figsize=(width_num, height_num), dpi=dpi) + + # Load and display the image using Matplotlib + axs.imshow(img) + axs.axis('off') + if show_plot: + plt.show() + + if show_plot: + fig.show() + + # Handle saving if requested + if save_results: + saved_files = {} + output_prefix = params.get("Output_File", "sankey") + + # Save the static plot + static_file = f"{output_prefix}_static.png" + static.savefig(static_file, dpi=dpi, bbox_inches='tight') + saved_files[static_file] = static_file + + # Save the interactive plot + interactive_file = f"{output_prefix}_interactive.html" + pio.write_html(fig, interactive_file) + saved_files[interactive_file] = interactive_file + + # Save the intermediate PNG that was created + saved_files[image_path] = image_path + + # Close figures after saving + plt.close(static) + + print(f"Sankey Plot completed → {list(saved_files.keys())}") + return saved_files + + return None + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python sankey_plot_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nPlots displayed (not saved)") \ No newline at end of file diff --git a/tests/templates/test_sankey_plot_template.py b/tests/templates/test_sankey_plot_template.py new file mode 100644 index 00000000..d8557241 --- /dev/null +++ b/tests/templates/test_sankey_plot_template.py @@ -0,0 +1,199 @@ +# tests/templates/test_sankey_plot_template.py +"""Unit tests for the Sankey Plot template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import matplotlib +matplotlib.use("Agg") # Headless backend for CI + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.sankey_plot_template import run_from_json + + +def mock_adata(n_cells: int = 20) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + + # Create observations with source and target annotations + # Ensure balanced distribution for sankey plot + source_types = ["ClusterA", "ClusterB", "ClusterC"] + target_types = ["PhenotypeX", "PhenotypeY", "PhenotypeZ"] + + # Create source annotations with proper repetition + source_pattern = source_types * ( + (n_cells + len(source_types) - 1) // len(source_types) + ) + source_annotation = source_pattern[:n_cells] + + # Create target annotations with some mixing + target_annotation = [] + for i in range(n_cells): + if i % 3 == 0: + target_annotation.append(target_types[0]) + elif i % 3 == 1: + target_annotation.append(target_types[1]) + else: + target_annotation.append(target_types[2]) + + obs = pd.DataFrame({ + "phenograph_k60_r1": source_annotation, + "renamed_phenotypes": target_annotation + }) + + x_mat = rng.normal(size=(n_cells, 3)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = ["Marker1", "Marker2", "Marker3"] + + return adata + + +class TestSankeyPlotTemplate(unittest.TestCase): + """Unit tests for the Sankey Plot template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "sankey" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters from NIDAP template + self.params = { + "Upstream_Analysis": self.in_file, + "Source_Annotation_Name": "phenograph_k60_r1", + "Target_Annotation_Name": "renamed_phenotypes", + "Source_Annotation_Color_Map": "tab20", + "Target_Annotation_Color_Map": "tab20b", + "Figure_Width_inch": 6, + "Figure_Height_inch": 6, + "Figure_DPI": 300, + "Font_Size": 12, + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + @patch('spac.templates.sankey_plot_template.sankey_plot') + @patch('plotly.io.write_image') + @patch('plotly.io.write_html') + @patch('matplotlib.pyplot.imread') + def test_complete_io_workflow( + self, mock_imread, mock_write_html, mock_write_image, mock_sankey + ) -> None: + """Single I/O test covering input/output scenarios.""" + # Mock the sankey_plot function to return a plotly figure + mock_fig = MagicMock() + mock_fig.update_layout = MagicMock() + mock_fig.show = MagicMock() + mock_sankey.return_value = mock_fig + + # Mock plt.imread to return a dummy image array + mock_imread.return_value = np.zeros((100, 100, 3)) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters + result = run_from_json(self.params, show_plot=False) + self.assertIsInstance(result, dict) + + # Verify multiple files were saved + self.assertIn("sankey_static.png", result) + self.assertIn("sankey_interactive.html", result) + self.assertIn("sankey_diagram.png", result) + + # Test 2: Run without saving + result_no_save = run_from_json( + self.params, save_results=False, show_plot=False + ) + # Should return None for multi-plot template + self.assertIsNone(result_no_save) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path, show_plot=False) + self.assertIsInstance(result_json, dict) + + # Verify sankey_plot was called with correct parameters + mock_sankey.assert_called() + call_args = mock_sankey.call_args + self.assertEqual( + call_args[1]['source_annotation'], "phenograph_k60_r1" + ) + self.assertEqual( + call_args[1]['target_annotation'], "renamed_phenotypes" + ) + self.assertEqual(call_args[1]['source_color_map'], "tab20") + self.assertEqual(call_args[1]['target_color_map'], "tab20b") + self.assertEqual(call_args[1]['sankey_font'], 12) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test missing annotation column + params_bad = self.params.copy() + params_bad["Source_Annotation_Name"] = "nonexistent_column" + + # This should trigger an error in the sankey_plot function + # For this test, we'll check that parameters are processed correctly + with patch('spac.templates.sankey_plot_template.sankey_plot') as \ + mock_sankey: + mock_sankey.side_effect = KeyError("nonexistent_column") + + with self.assertRaises(KeyError) as context: + run_from_json(params_bad) + + self.assertIn("nonexistent_column", str(context.exception)) + + @patch('spac.templates.sankey_plot_template.sankey_plot') + @patch('plotly.io.write_image') + @patch('plotly.io.write_html') + @patch('matplotlib.pyplot.imread') + def test_function_calls( + self, mock_imread, mock_write_html, mock_write_image, mock_sankey + ) -> None: + """Test that main function is called with correct parameters.""" + # Mock the plotly figure + mock_fig = MagicMock() + mock_sankey.return_value = mock_fig + + # Mock plt.imread to return a dummy image array + mock_imread.return_value = np.zeros((100, 100, 3)) + + # Test with None annotations (should use text_to_value) + params_none = self.params.copy() + params_none["Source_Annotation_Name"] = "None" + params_none["Target_Annotation_Name"] = "None" + + run_from_json(params_none, save_results=False, show_plot=False) + + # Verify function was called with None values + call_args = mock_sankey.call_args + self.assertIsNone(call_args[1]['source_annotation']) + self.assertIsNone(call_args[1]['target_annotation']) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 07baeb9037fd6bc3cefee2b71abb1b2d777223d9 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sun, 3 Aug 2025 00:35:32 -0400 Subject: [PATCH 052/102] fix(sankey_plot_template): address the comments from copilot --- src/spac/templates/sankey_plot_template.py | 6 ++++-- tests/templates/test_sankey_plot_template.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/spac/templates/sankey_plot_template.py b/src/spac/templates/sankey_plot_template.py index 5e544216..60d4aa34 100644 --- a/src/spac/templates/sankey_plot_template.py +++ b/src/spac/templates/sankey_plot_template.py @@ -71,7 +71,7 @@ def run_from_json( height_num = params.get("Figure_Height_inch", 6) height_in_pixels = height_num / scale * dpi - sort_asscend = True + # sort_asscend = True # unused variable source_color_map = params.get("Source_Annotation_Color_Map", "tab20") target_color_map = params.get("Target_Annotation_Color_Map", "tab20b") @@ -98,7 +98,9 @@ def run_from_json( # Show the plot with the specified display options print(fig) - image_path = 'sankey_diagram.png' + # Use output prefix to avoid conflicts + output_prefix = params.get("Output_File", "sankey") + image_path = f"{output_prefix}_diagram.png" pio.write_image( fig, diff --git a/tests/templates/test_sankey_plot_template.py b/tests/templates/test_sankey_plot_template.py index d8557241..198d1ba4 100644 --- a/tests/templates/test_sankey_plot_template.py +++ b/tests/templates/test_sankey_plot_template.py @@ -157,7 +157,7 @@ def test_error_validation(self) -> None: params_bad["Source_Annotation_Name"] = "nonexistent_column" # This should trigger an error in the sankey_plot function - # For this test, we'll check that parameters are processed correctly + # This test will check that parameters are processed correctly with patch('spac.templates.sankey_plot_template.sankey_plot') as \ mock_sankey: mock_sankey.side_effect = KeyError("nonexistent_column") From 824d131fed2216a5a291f61fc53d53e5e2c98c11 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sun, 3 Aug 2025 01:10:08 -0400 Subject: [PATCH 053/102] feat(neighborhood_profile_template): add neighborhood_profile_template function and unit tests --- .../neighborhood_profile_template.py | 237 ++++++++++++++++++ .../test_neighborhood_profile_template.py | 208 +++++++++++++++ 2 files changed, 445 insertions(+) create mode 100644 src/spac/templates/neighborhood_profile_template.py create mode 100644 tests/templates/test_neighborhood_profile_template.py diff --git a/src/spac/templates/neighborhood_profile_template.py b/src/spac/templates/neighborhood_profile_template.py new file mode 100644 index 00000000..0bc74861 --- /dev/null +++ b/src/spac/templates/neighborhood_profile_template.py @@ -0,0 +1,237 @@ +""" +Platform-agnostic Neighborhood Profile template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.neighborhood_profile_template import run_from_json +>>> run_from_json("examples/neighborhood_profile_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, List, Optional, Tuple +import pandas as pd +import numpy as np + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.spatial_analysis import neighborhood_profile +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], Dict[Tuple[str, str], pd.DataFrame]]: + """ + Execute Neighborhood Profile analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the dataframes + directly for in-memory workflows. Default is True. + + Returns + ------- + dict + If save_results=True: Dictionary of saved file paths + If save_results=False: Dictionary of (anchor, neighbor) tuples + to DataFrames + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + cell_types_annotation = params["Annotation_of_interest"] + bins = params["Bins"] + slide_names = params.get("Stratify_By", "None") + normalization = None + output_table = "neighborhood_profile" + + anchor_neighbor_list = params["Anchor_Neighbor_List"] + anchor_neighbor_list = [ + tuple(map(str.strip, item.split(";"))) + for item in anchor_neighbor_list + ] + + # Call the spatial umap calculation + + bins = [float(radius) for radius in bins] + + slide_names = text_to_value(slide_names) + + neighborhood_profile( + adata, + phenotypes=cell_types_annotation, + distances=bins, + regions=slide_names, + spatial_key="spatial", + normalize=normalization, + associated_table_name=output_table + ) + + print(adata) + print(adata.obsm[output_table].shape) + print(adata.uns[output_table]) + + dataframes, filenames = neighborhood_profiles_for_pairs( + adata, + cell_types_annotation, + slide_names, + bins, + anchor_neighbor_list, + output_table + ) + + # Handle results based on save_results flag + if save_results: + # Save outputs + saved_files = {} + + for (anchor_label, neighbor_label), filename in zip( + dataframes.keys(), filenames + ): + df = dataframes[(anchor_label, neighbor_label)] + saved_files[filename] = df + + # Save all CSV files + saved_files = save_outputs(saved_files) + + for filename in saved_files: + print(f"Saved: {filename}") + + print( + f"Neighborhood Profile completed → {len(saved_files)} files" + ) + return saved_files + else: + # Return the dataframes directly for in-memory workflows + print("Returning dataframes (not saving to file)") + return dataframes + + +# Global imports and functions included below + +def neighborhood_profiles_for_pairs( + adata, + cell_types_annotation, + slide_names, + bins, + anchor_neighbor_list, + output_table +): + """ + Compute neighborhood profiles for all anchor-neighbor pairs and return + a tuple containing a dictionary of DataFrames and a list of filenames + for saving. + + Parameters + ---------- + adata : AnnData + The AnnData object containing spatial and phenotypic data. + + cell_types_annotation : str + The column name in adata.obs containing the cell phenotype labels. + + slide_names : str + The column name in adata.obs containing the slide names. + + bins : list + List of increasing distance bins. + + anchor_neighbor_list : list of tuples + List of (anchor_label, neighbor_label) pairs. + + output_table : str + The key in adata.obsm containing neighborhood profile data. + + Returns + ------- + tuple + - A dictionary of DataFrames for each (anchor, neighbor) pair. + - A list of filenames where each DataFrame should be saved. + """ + + dataframes = {} + filenames = [] + + # Get the array of neighbor labels + neighbor_labels = adata.uns[output_table]["labels"] + + for anchor_label, neighbor_label in anchor_neighbor_list: + # Create bin labels with the neighbor type + bins_with_ranges = [ + f"{neighbor_label}_{bins[i]}-{bins[i+1]}" + for i in range(len(bins) - 1) + ] + + # Find the index of the requested neighbor label + neighbor_index = np.where(neighbor_labels == neighbor_label)[0] + + if len(neighbor_index) == 0: + raise ValueError( + f"Neighbor label '{neighbor_label}' not found in " + f"{output_table} labels." + ) + + neighbor_index = neighbor_index[0] # Extract the first index + + # Extract the neighborhood profile for the specific neighbor + # Shape: (n_cells, n_bins) + profile_data = adata.obsm[output_table][:, neighbor_index, :] + + # Construct DataFrame + df = pd.DataFrame(profile_data, columns=bins_with_ranges) + + # Add cell phenotype labels and slide names + df.insert( + 0, cell_types_annotation, + adata.obs[cell_types_annotation].values + ) + if slide_names is not None: + df.insert(0, slide_names, adata.obs[slide_names].values) + + # Filter for the anchor cell type + filtered_df = df[df[cell_types_annotation] == anchor_label] + + # Generate a filename for saving + filename = f"anchor_{anchor_label}_neighbor_{neighbor_label}.csv" + + # Store the DataFrame and filename + dataframes[(anchor_label, neighbor_label)] = filtered_df + filenames.append(filename) + + return dataframes, filenames + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python neighborhood_profile_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned dataframes") \ No newline at end of file diff --git a/tests/templates/test_neighborhood_profile_template.py b/tests/templates/test_neighborhood_profile_template.py new file mode 100644 index 00000000..e286771b --- /dev/null +++ b/tests/templates/test_neighborhood_profile_template.py @@ -0,0 +1,208 @@ +# tests/templates/test_neighborhood_profile_template.py +"""Unit tests for the Neighborhood Profile template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.neighborhood_profile_template import run_from_json + + +def mock_adata(n_cells: int = 20) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + + # Create simple cell types that repeat properly for any n_cells + cell_types = ["T cells", "B cells", "Tumor cells"] + obs = pd.DataFrame({ + "cell_type": [cell_types[i % len(cell_types)] + for i in range(n_cells)], + "slide": (["Slide1", "Slide2"] * ((n_cells + 1) // 2))[:n_cells] + }) + + x_mat = rng.normal(size=(n_cells, 3)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = ["Marker1", "Marker2", "Marker3"] + + # Add spatial coordinates + adata.obsm["spatial"] = rng.random((n_cells, 2)) * 100 + + return adata + + +class TestNeighborhoodProfileTemplate(unittest.TestCase): + """Unit tests for the Neighborhood Profile template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters from NIDAP template + self.params = { + "Upstream_Analysis": self.in_file, + "Annotation_of_interest": "cell_type", + "Bins": [0, 10, 30, 50], + "Stratify_By": "slide", + "Anchor_Neighbor_List": [ + "T cells;B cells", + "Tumor cells;T cells" + ] + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Mock the neighborhood_profile function to add expected data + with patch('spac.templates.neighborhood_profile_template.' + 'neighborhood_profile') as mock_np: + # Setup mock to add the expected data structures + def mock_neighborhood_profile(adata, **kwargs): + # Add mock neighborhood profile data + n_cells = adata.n_obs + n_phenotypes = 3 # T cells, B cells, Tumor cells + n_bins = len(kwargs['distances']) - 1 # 3 bins + + # Create mock profile data + adata.obsm["neighborhood_profile"] = np.random.rand( + n_cells, n_phenotypes, n_bins + ) + + # Add labels to uns + adata.uns["neighborhood_profile"] = { + "labels": np.array([ + "T cells", "B cells", "Tumor cells" + ]) + } + + mock_np.side_effect = mock_neighborhood_profile + + # Test 1: Run with default parameters (save files) + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + + # Should have 2 CSV files based on anchor_neighbor_list + self.assertEqual(len(result), 2) + + # Check filenames + expected_files = [ + "anchor_T cells_neighbor_B cells.csv", + "anchor_Tumor cells_neighbor_T cells.csv" + ] + for expected in expected_files: + self.assertIn(expected, result) + + # Test 2: Run without saving + result_no_save = run_from_json( + self.params, save_results=False + ) + # Should return dict of dataframes + self.assertIsInstance(result_no_save, dict) + self.assertEqual(len(result_no_save), 2) + + # Check that keys are tuples + for key in result_no_save: + self.assertIsInstance(key, tuple) + self.assertEqual(len(key), 2) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + self.assertEqual(len(result_json), 2) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Mock neighborhood_profile to add required data structures + with patch('spac.templates.neighborhood_profile_template.' + 'neighborhood_profile') as mock_np: + def mock_neighborhood_profile(adata, **kwargs): + adata.obsm["neighborhood_profile"] = np.random.rand( + adata.n_obs, 2, 3 # Only 2 phenotypes + ) + adata.uns["neighborhood_profile"] = { + "labels": np.array([ + "T cells", "B cells" + ]) # Missing "Unknown" + } + + mock_np.side_effect = mock_neighborhood_profile + + # Test with invalid neighbor label + params_bad = self.params.copy() + params_bad["Anchor_Neighbor_List"] = ["T cells;Unknown"] + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + # Check exact error message + expected_msg = ("Neighbor label 'Unknown' not found in " + "neighborhood_profile labels.") + self.assertEqual(str(context.exception), expected_msg) + + @patch('spac.templates.neighborhood_profile_template.' + 'neighborhood_profile') + def test_function_calls(self, mock_np) -> None: + """Test that main function is called with correct parameters.""" + # Mock the neighborhood_profile function + def mock_neighborhood_profile(adata, **kwargs): + # Add required data structures + adata.obsm["neighborhood_profile"] = np.zeros( + (adata.n_obs, 3, 3) + ) + adata.uns["neighborhood_profile"] = { + "labels": np.array(["T cells", "B cells", "Tumor cells"]) + } + + mock_np.side_effect = mock_neighborhood_profile + + # Test with None slide_names + params_alt = self.params.copy() + params_alt["Stratify_By"] = "None" + + run_from_json(params_alt, save_results=False) + + # Verify function was called correctly + mock_np.assert_called_once() + call_args = mock_np.call_args + + # Check specific parameters + self.assertEqual(call_args[1]['phenotypes'], "cell_type") + self.assertEqual(call_args[1]['distances'], [0.0, 10.0, 30.0, 50.0]) + self.assertIsNone(call_args[1]['regions']) # "None" -> None + self.assertEqual(call_args[1]['spatial_key'], "spatial") + self.assertIsNone(call_args[1]['normalize']) + self.assertEqual( + call_args[1]['associated_table_name'], "neighborhood_profile" + ) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 448a980c496dfa5295a33e4432649947da6e6af7 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sun, 3 Aug 2025 01:37:05 -0400 Subject: [PATCH 054/102] feat(analysis_to_csv_template): add analysis_to_csv_template function and unit tests --- .../templates/analysis_to_csv_template.py | 137 ++++++++++++++++++ .../test_analysis_to_csv_template.py | 137 ++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 src/spac/templates/analysis_to_csv_template.py create mode 100644 tests/templates/test_analysis_to_csv_template.py diff --git a/src/spac/templates/analysis_to_csv_template.py b/src/spac/templates/analysis_to_csv_template.py new file mode 100644 index 00000000..8a2f35d8 --- /dev/null +++ b/src/spac/templates/analysis_to_csv_template.py @@ -0,0 +1,137 @@ +""" +Platform-agnostic Analysis to CSV template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.analysis_to_csv_template import run_from_json +>>> run_from_json("examples/analysis_to_csv_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union +import pandas as pd + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.utils import check_table +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], pd.DataFrame]: + """ + Execute Analysis to CSV analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the dataframe + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or DataFrame + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed DataFrame + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + input_layer = params.get("Table_to_Export", "Original") + save_file = params.get("Save_as_CSV_File", False) + + if input_layer == "Original": + input_layer = None + + def export_layer_to_csv( + adata, + layer=None): + """ + Exports the specified layer or the default .X data matrix of an + AnnData object to a CSV file. + """ + # Check if the provided layer exists in the AnnData object + if layer: + check_table(adata, tables=layer) + data_to_export = pd.DataFrame( + adata.layers[layer], + index=adata.obs.index, + columns=adata.var.index + ) + else: + data_to_export = pd.DataFrame( + adata.X, + index=adata.obs.index, + columns=adata.var.index + ) + + # Join with the observation metadata + full_data_df = data_to_export.join(adata.obs) + + # Join the spatial coordinates + + # Extract the spatial coordinates + spatial_df = pd.DataFrame( + adata.obsm['spatial'], + index=adata.obs.index, + columns=['spatial_x', 'spatial_y'] + ) + + # Join spatial_df with full_data_df + full_data_df = full_data_df.join(spatial_df) + + return(full_data_df) + + csv_data = export_layer_to_csv( + adata=adata, + layer=input_layer + ) + + # Handle results based on save_results flag and save_file parameter + if save_results and save_file: + # Save outputs + output_file = params.get("Output_File", "analysis.csv") + saved_files = save_outputs({output_file: csv_data}) + + print(f"Analysis to CSV completed → {saved_files[output_file]}") + return saved_files + else: + print(csv_data.info()) + # Return the dataframe directly for in-memory workflows + return csv_data + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python analysis_to_csv_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned DataFrame") \ No newline at end of file diff --git a/tests/templates/test_analysis_to_csv_template.py b/tests/templates/test_analysis_to_csv_template.py new file mode 100644 index 00000000..7a9bdb53 --- /dev/null +++ b/tests/templates/test_analysis_to_csv_template.py @@ -0,0 +1,137 @@ +# tests/templates/test_analysis_to_csv_template.py +"""Unit tests for the Analysis to CSV template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.analysis_to_csv_template import run_from_json + + +def mock_adata(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], + "batch": (["Batch1", "Batch2"] * ((n_cells + 1) // 2))[:n_cells] + }) + x_mat = rng.normal(size=(n_cells, 3)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = ["Marker1", "Marker2", "Marker3"] + # Add spatial coordinates + adata.obsm["spatial"] = rng.random((n_cells, 2)) * 100 + # Add a test layer + adata.layers["normalized"] = rng.normal(size=(n_cells, 3)) + return adata + + +class TestAnalysisToCSVTemplate(unittest.TestCase): + """Unit tests for the Analysis to CSV template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "analysis.csv" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters - from NIDAP template + self.params = { + "Upstream_Analysis": self.in_file, + "Table_to_Export": "Original", + "Save_as_CSV_File": False, + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with Save_as_CSV_File=False (returns DataFrame) + result = run_from_json(self.params) + self.assertIsInstance(result, pd.DataFrame) + # Verify dataframe has expected columns + self.assertIn("Marker1", result.columns) + self.assertIn("Marker2", result.columns) + self.assertIn("Marker3", result.columns) + self.assertIn("cell_type", result.columns) + self.assertIn("spatial_x", result.columns) + self.assertIn("spatial_y", result.columns) + + # Test 2: Run with Save_as_CSV_File=True + params_save = self.params.copy() + params_save["Save_as_CSV_File"] = True + result_save = run_from_json(params_save) + self.assertIsInstance(result_save, dict) + self.assertIn(self.out_file, result_save) + + # Test 3: Export specific layer + params_layer = self.params.copy() + params_layer["Table_to_Export"] = "normalized" + result_layer = run_from_json(params_layer) + self.assertIsInstance(result_layer, pd.DataFrame) + # Verify it has the normalized data + self.assertEqual(len(result_layer), 10) # n_cells + + # Test 4: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, pd.DataFrame) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test missing layer + params_bad = self.params.copy() + params_bad["Table_to_Export"] = "nonexistent_layer" + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + # Check that error mentions the missing layer + self.assertIn("nonexistent_layer", str(context.exception)) + + @patch('spac.templates.analysis_to_csv_template.check_table') + def test_function_calls(self, mock_check) -> None: + """Test that main function is called with correct parameters.""" + # Run with a specific layer + params_with_layer = self.params.copy() + params_with_layer["Table_to_Export"] = "normalized" + + result = run_from_json(params_with_layer) + + # Verify check_table was called for the layer + mock_check.assert_called_once() + call_args = mock_check.call_args + self.assertEqual(call_args[1]['tables'], "normalized") + + # Verify result is a DataFrame + self.assertIsInstance(result, pd.DataFrame) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 34961bde03bfc47211047ec58cbacc0814712e3c Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sun, 3 Aug 2025 02:20:13 -0400 Subject: [PATCH 055/102] feat(summarize_annotation_statistics): add summarize_annotation_statistics template function and unit tests --- ...ummarize_annotation_statistics_template.py | 123 ++++++++++++ ...ummarize_annotation_statistics_template.py | 176 ++++++++++++++++++ 2 files changed, 299 insertions(+) create mode 100644 src/spac/templates/summarize_annotation_statistics_template.py create mode 100644 tests/templates/test_summarize_annotation_statistics_template.py diff --git a/src/spac/templates/summarize_annotation_statistics_template.py b/src/spac/templates/summarize_annotation_statistics_template.py new file mode 100644 index 00000000..cd7c4ff3 --- /dev/null +++ b/src/spac/templates/summarize_annotation_statistics_template.py @@ -0,0 +1,123 @@ +""" +Platform-agnostic Summarize Annotation's Statistics template converted from +NIDAP. Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.summarize_annotation_statistics_template import \ +... run_from_json +>>> run_from_json("examples/summarize_annotation_statistics_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, List, Optional, Tuple +import pandas as pd + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.transformations import get_cluster_info +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], pd.DataFrame]: + """ + Execute Summarize Annotation's Statistics analysis with parameters from + JSON. Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the dataframe + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or DataFrame + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed DataFrame + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + layer = params.get("Table_to_Process", "Original") + features = params.get("Feature_s_", ["All"]) + annotation = params.get("Annotation", "None") + + if layer == "Original": + layer = None + + if len(features) == 1 and features[0] == "All": + features = None + + if annotation == "None": + annotation = None + + info = get_cluster_info( + adata=adata, + layer=layer, + annotation=annotation, + features=features + ) + + df = pd.DataFrame(info) + + # Renaming columns to avoid spaces and special characters + # Assuming `info` is a pandas DataFrame + df.columns = [ + col.replace(" ", "_").replace("-", "_") for col in df.columns + ] + + # Get summary statistics of returned dataset + print("Summary statistics of the dataset:", df.describe()) + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "annotation_summaries.csv") + saved_files = save_outputs({output_file: df}) + + print( + f"Summarize Annotation's Statistics completed → " + f"{saved_files[output_file]}" + ) + return saved_files + else: + # Return the dataframe directly for in-memory workflows + print("Returning DataFrame (not saving to file)") + return df + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python summarize_annotation_statistics_template.py " + "", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned DataFrame") \ No newline at end of file diff --git a/tests/templates/test_summarize_annotation_statistics_template.py b/tests/templates/test_summarize_annotation_statistics_template.py new file mode 100644 index 00000000..97904a52 --- /dev/null +++ b/tests/templates/test_summarize_annotation_statistics_template.py @@ -0,0 +1,176 @@ +# tests/templates/test_summarize_annotation_statistics_template.py +"""Unit tests for the Summarize Annotation's Statistics template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.summarize_annotation_statistics_template import ( + run_from_json +) + + +def mock_adata(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], + "cluster": ([f"C{i % 3}" for i in range(n_cells)])[:n_cells] + }) + x_mat = rng.normal(size=(n_cells, 5)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = [f"Gene{i}" for i in range(5)] + # Add a layer for testing + adata.layers["normalized"] = x_mat * 2.0 + return adata + + +class TestSummarizeAnnotationStatisticsTemplate(unittest.TestCase): + """Unit tests for the Summarize Annotation's Statistics template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "annotation_summaries.csv" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters from NIDAP template + self.params = { + "Upstream_Analysis": self.in_file, + "Table_to_Process": "Original", + "Feature_s_": ["All"], + "Annotation": "cell_type", + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + # Verify CSV file was saved + self.assertIn(self.out_file, result) + csv_path = result[self.out_file] + self.assertTrue(os.path.exists(csv_path)) + + # Verify CSV content structure + df_saved = pd.read_csv(csv_path) + # Should have renamed columns (no spaces/hyphens) + for col in df_saved.columns: + self.assertNotIn(" ", col) + self.assertNotIn("-", col) + + # Test 2: Run without saving + result_no_save = run_from_json(self.params, save_results=False) + # Check appropriate return type + self.assertIsInstance(result_no_save, pd.DataFrame) + self.assertGreater(len(result_no_save), 0) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + @patch('spac.templates.summarize_annotation_statistics_template.' + 'get_cluster_info') + def test_function_calls(self, mock_get_cluster_info) -> None: + """Test that main function is called with correct parameters.""" + # Mock the main function to return a simple DataFrame + mock_df = pd.DataFrame({ + "cluster": ["TypeA", "TypeB"], + "cell count": [5, 5], + "mean-expression": [1.0, 2.0] + }) + mock_get_cluster_info.return_value = mock_df + + # Test with different parameter combinations + + # Test 1: Default parameters + run_from_json(self.params, save_results=False) + mock_get_cluster_info.assert_called_once_with( + adata=mock_get_cluster_info.call_args[1]['adata'], + layer=None, # "Original" → None + annotation="cell_type", + features=None # ["All"] → None + ) + + # Test 2: With specific features and layer + mock_get_cluster_info.reset_mock() + params_features = self.params.copy() + params_features["Feature_s_"] = ["Gene1", "Gene2"] + params_features["Table_to_Process"] = "normalized" + params_features["Annotation"] = "cluster" + + run_from_json(params_features, save_results=False) + mock_get_cluster_info.assert_called_once_with( + adata=mock_get_cluster_info.call_args[1]['adata'], + layer="normalized", + annotation="cluster", + features=["Gene1", "Gene2"] + ) + + # Test 3: With "None" annotation + mock_get_cluster_info.reset_mock() + params_none = self.params.copy() + params_none["Annotation"] = "None" + + run_from_json(params_none, save_results=False) + mock_get_cluster_info.assert_called_once_with( + adata=mock_get_cluster_info.call_args[1]['adata'], + layer=None, + annotation=None, # "None" → None + features=None + ) + + def test_column_renaming(self) -> None: + """Test that columns with spaces and hyphens are renamed.""" + with patch('spac.templates.summarize_annotation_statistics_template.' + 'get_cluster_info') as mock_func: + # Create a DataFrame with problematic column names + mock_df = pd.DataFrame({ + "cell type": ["A", "B"], + "mean-expression": [1.0, 2.0], + "std dev": [0.1, 0.2], + "CD4+ count": [10, 20] + }) + mock_func.return_value = mock_df + + # Run without saving to get the DataFrame directly + result_df = run_from_json(self.params, save_results=False) + + # Check that columns are renamed + expected_columns = ["cell_type", "mean_expression", "std_dev", + "CD4+_count"] + self.assertEqual(list(result_df.columns), expected_columns) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 3f4336bc75f04ef1f4f8ca740a8b9b678a288da2 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sun, 3 Aug 2025 19:15:19 -0400 Subject: [PATCH 056/102] feat(interactive_spatial_plot_template): add interactive_spatial_plot_template function and unit tests --- .../interactive_spatial_plot_template.py | 184 ++++++++++++++++ .../test_interactive_spatial_plot_template.py | 200 ++++++++++++++++++ 2 files changed, 384 insertions(+) create mode 100644 src/spac/templates/interactive_spatial_plot_template.py create mode 100644 tests/templates/test_interactive_spatial_plot_template.py diff --git a/src/spac/templates/interactive_spatial_plot_template.py b/src/spac/templates/interactive_spatial_plot_template.py new file mode 100644 index 00000000..1461d0d6 --- /dev/null +++ b/src/spac/templates/interactive_spatial_plot_template.py @@ -0,0 +1,184 @@ +""" +Platform-agnostic Interactive Spatial Plot template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.interactive_spatial_plot_template import run_from_json +>>> run_from_json("examples/interactive_spatial_plot_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, List, Optional +import pandas as pd +import plotly.io as pio + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +# Import SPAC functions from NIDAP template +from spac.visualization import interactive_spatial_plot +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Optional[Dict[str, str]]: + """ + Execute Interactive Spatial Plot analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. Default is True. + + Returns + ------- + dict or None + If save_results=True: Dictionary of saved file paths + If save_results=False: None (plots are shown interactively) + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + ## -------------------------------- ## + ## User-Defined Template Parameters ## + ## -------------------------------- ## + + color_by = params["Color_By"] + annotations = params.get("Annotation_s_to_Highlight", [""]) + feature = params.get("Feature_to_Highlight", "None") + layer = params.get("Table", "Original") + + dot_size = params.get("Dot_Size", 1.5) + dot_transparency = params.get("Dot_Transparency", 0.75) + color_map = params.get("Feature_Color_Scale", "balance") + desired_width_in = params.get("Figure_Width", 6) + desired_height_in = params.get("Figure_Height", 4) + dpi = params.get("Figure_DPI", 200) + Font_size = params.get("Font_Size", 12) + stratify_by = text_to_value( + params.get("Stratify_By", "None"), + param_name="Stratify By" + ) # New parameter for stratification + + defined_color_map = text_to_value( + params.get("Define_Label_Color_Mapping", "None"), + param_name="Define Label Color Mapping" + ) + + cmin = params.get("Lower_Colorbar_Bound", 999) + cmax = params.get("Upper_Colorbar_Bound", -999) + + flip_y = params.get("Flip_Vertical_Axis", False) + + feature = text_to_value(feature) + if color_by == "Annotation": + feature = None + if len(annotations) == 0: + raise ValueError( + 'Please set at least one value in the ' + '"Annotation(s) to Highlight" parameter' + ) + else: + annotations = None + if feature is None: + raise ValueError('Please set the "Feature to Highlight" parameter.') + + layer = text_to_value(layer, "Original") + + ##--------------- ## + ## Error Messages ## + ## -------------- ## + + ## --------- ## + ## Functions ## + ## --------- ## + + ## --------------- ## + ## Main Code Block ## + ## --------------- ## + result_list = interactive_spatial_plot( + adata=adata, + annotations=annotations, + feature=feature, + layer=layer, + dot_size=dot_size, + dot_transparency=dot_transparency, + feature_colorscale=color_map, + figure_width=desired_width_in, + figure_height=desired_height_in, + figure_dpi=dpi, + font_size=Font_size, + stratify_by=stratify_by, + defined_color_map=defined_color_map, + reverse_y_axis=flip_y, + cmin=cmin, + cmax=cmax + ) + + # Handle results based on save_results flag + if save_results: + saved_files = {} + output_prefix = params.get("Output_File", "interactive_plot") + + for result in result_list: + image_name = result['image_name'] + image_object = result['image_object'] + + # Show the plot (as in NIDAP template) + image_object.show() + + # Convert to HTML + html_content = pio.to_html(image_object, full_html=True) + + # Save HTML file + html_filename = f"{output_prefix}_{image_name}.html" + with open(html_filename, 'w') as file: + file.write(html_content) + + saved_files[html_filename] = html_filename + + print( + f"Interactive Spatial Plot completed → " + f"{list(saved_files.keys())}" + ) + return saved_files + else: + # Just show the plots without saving + for result in result_list: + result['image_object'].show() + + return None + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python interactive_spatial_plot_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nDisplayed interactive plots") \ No newline at end of file diff --git a/tests/templates/test_interactive_spatial_plot_template.py b/tests/templates/test_interactive_spatial_plot_template.py new file mode 100644 index 00000000..709eb345 --- /dev/null +++ b/tests/templates/test_interactive_spatial_plot_template.py @@ -0,0 +1,200 @@ +# tests/templates/test_interactive_spatial_plot_template.py +"""Unit tests for the Interactive Spatial Plot template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.interactive_spatial_plot_template import run_from_json + + +def mock_adata(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "renamed_phenotypes": ( + ["TypeA", "TypeB"] * ((n_cells + 1) // 2) + )[:n_cells], + "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] + }) + x_mat = rng.normal(size=(n_cells, 3)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = ["Gene1", "Gene2", "Gene3"] + # Add spatial coordinates required for spatial plots + adata.obsm["spatial"] = rng.random((n_cells, 2)) * 100 + # Add color mapping + adata.uns["_spac_colors"] = { + "TypeA": "#FF0000", + "TypeB": "#0000FF" + } + return adata + + +class TestInteractiveSpatialPlotTemplate(unittest.TestCase): + """Unit tests for the Interactive Spatial Plot template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "interactive_plot" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters - adjust based on template + self.params = { + "Upstream_Analysis": self.in_file, + "Color_By": "Annotation", + "Annotation_s_to_Highlight": ["renamed_phenotypes"], + "Feature_to_Highlight": "None", + "Table": "Original", + "Dot_Size": 3, + "Dot_Transparency": 0.75, + "Feature_Color_Scale": "balance", + "Figure_Width": 12, + "Figure_Height": 12, + "Figure_DPI": 200, + "Font_Size": 12, + "Stratify_By": "None", + "Define_Label_Color_Mapping": "_spac_colors", + "Lower_Colorbar_Bound": 999, + "Upper_Colorbar_Bound": -999, + "Flip_Vertical_Axis": False, + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + @patch('spac.templates.interactive_spatial_plot_template.' + 'interactive_spatial_plot') + @patch('plotly.io.to_html') + def test_complete_io_workflow( + self, + mock_to_html, + mock_spatial_plot, + ) -> None: + """Single I/O test covering input/output scenarios.""" + # Mock the interactive_spatial_plot function + mock_fig1 = MagicMock() + mock_fig1.show = MagicMock() + + mock_spatial_plot.return_value = [ + { + 'image_name': 'plot_1', + 'image_object': mock_fig1 + } + ] + + # Mock HTML conversion + mock_to_html.return_value = "Mock Plot" + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + self.assertEqual(len(result), 1) + expected_file = f"{self.out_file}_plot_1.html" + self.assertIn(expected_file, result) + + # Test 2: Run without saving + result_no_save = run_from_json(self.params, save_results=False) + self.assertIsNone(result_no_save) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + # Verify interactive_spatial_plot was called correctly + mock_spatial_plot.assert_called() + call_args = mock_spatial_plot.call_args[1] + self.assertEqual(call_args['annotations'], ["renamed_phenotypes"]) + self.assertIsNone(call_args['feature']) + self.assertEqual(call_args['dot_size'], 3) + self.assertEqual(call_args['reverse_y_axis'], False) + + def test_error_validation(self) -> None: + """Test exact error messages for invalid parameters.""" + # Test missing annotation when Color_By is "Annotation" + params_bad = self.params.copy() + params_bad["Annotation_s_to_Highlight"] = [] + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + expected_msg = ( + 'Please set at least one value in the "Annotation(s) to ' + 'Highlight" parameter' + ) + self.assertEqual(str(context.exception), expected_msg) + + # Test missing feature when Color_By is "Feature" + params_bad2 = self.params.copy() + params_bad2["Color_By"] = "Feature" + params_bad2["Feature_to_Highlight"] = "None" + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad2) + + expected_msg = 'Please set the "Feature to Highlight" parameter.' + self.assertEqual(str(context.exception), expected_msg) + + @patch('spac.templates.interactive_spatial_plot_template.' + 'interactive_spatial_plot') + def test_function_calls(self, mock_spatial_plot) -> None: + """Test that main function is called with correct parameters.""" + # Mock multiple plots with stratification + mock_fig1 = MagicMock() + mock_fig2 = MagicMock() + + mock_spatial_plot.return_value = [ + {'image_name': 'S1', 'image_object': mock_fig1}, + {'image_name': 'S2', 'image_object': mock_fig2} + ] + + # Test with stratification + params_strat = self.params.copy() + params_strat["Stratify_By"] = "sample" + params_strat["Color_By"] = "Feature" + params_strat["Feature_to_Highlight"] = "Gene1" + params_strat["Annotation_s_to_Highlight"] = [""] + + with patch('plotly.io.to_html', return_value="Mock"): + run_from_json(params_strat) + + # Verify function was called correctly + mock_spatial_plot.assert_called_once() + call_args = mock_spatial_plot.call_args[1] + + # Check parameter conversions + self.assertIsNone(call_args['annotations']) + self.assertEqual(call_args['feature'], "Gene1") + self.assertEqual(call_args['stratify_by'], "sample") + self.assertEqual(call_args['defined_color_map'], "_spac_colors") + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 64ff3023155345b86ad9b72838bae0b53db21930 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sun, 3 Aug 2025 19:27:13 -0400 Subject: [PATCH 057/102] fix(interactive_spatial_plot_template): remove nidap comments --- .../interactive_spatial_plot_template.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/spac/templates/interactive_spatial_plot_template.py b/src/spac/templates/interactive_spatial_plot_template.py index 1461d0d6..eed1b014 100644 --- a/src/spac/templates/interactive_spatial_plot_template.py +++ b/src/spac/templates/interactive_spatial_plot_template.py @@ -54,10 +54,6 @@ def run_from_json( # Load the upstream analysis data adata = load_input(params["Upstream_Analysis"]) - ## -------------------------------- ## - ## User-Defined Template Parameters ## - ## -------------------------------- ## - color_by = params["Color_By"] annotations = params.get("Annotation_s_to_Highlight", [""]) feature = params.get("Feature_to_Highlight", "None") @@ -73,7 +69,7 @@ def run_from_json( stratify_by = text_to_value( params.get("Stratify_By", "None"), param_name="Stratify By" - ) # New parameter for stratification + ) defined_color_map = text_to_value( params.get("Define_Label_Color_Mapping", "None"), @@ -100,17 +96,6 @@ def run_from_json( layer = text_to_value(layer, "Original") - ##--------------- ## - ## Error Messages ## - ## -------------- ## - - ## --------- ## - ## Functions ## - ## --------- ## - - ## --------------- ## - ## Main Code Block ## - ## --------------- ## result_list = interactive_spatial_plot( adata=adata, annotations=annotations, From a7b13494e86b14555aba6a6fbcc0c7c12c1441f1 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sun, 3 Aug 2025 20:46:48 -0400 Subject: [PATCH 058/102] feat(spatial_interaction_template): add spatial_interaction_template and unit tests --- .../templates/spatial_interaction_template.py | 273 ++++++++++++++++++ .../test_spatial_interaction_template.py | 242 ++++++++++++++++ 2 files changed, 515 insertions(+) create mode 100644 src/spac/templates/spatial_interaction_template.py create mode 100644 tests/templates/test_spatial_interaction_template.py diff --git a/src/spac/templates/spatial_interaction_template.py b/src/spac/templates/spatial_interaction_template.py new file mode 100644 index 00000000..84d5c496 --- /dev/null +++ b/src/spac/templates/spatial_interaction_template.py @@ -0,0 +1,273 @@ +""" +Platform-agnostic Spatial Interaction template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.spatial_interaction_template import run_from_json +>>> run_from_json("examples/spatial_interaction_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, List, Optional +import pandas as pd +import numpy as np +from PIL import Image +from pprint import pprint +import matplotlib.pyplot as plt + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.spatial_analysis import spatial_interaction +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True, + show_plot: bool = True +) -> Union[Dict[str, str], None]: + """ + Execute Spatial Interaction analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. Default is True. + show_plot : bool, optional + Whether to display the plot. Default is True. + + Returns + ------- + dict or None + If save_results=True: Dictionary of saved file paths containing: + - PNG files: Heatmap visualizations of spatial interactions + - CSV files: Matrices with interaction scores/counts between cell types + If save_results=False: None (plots are displayed only) + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + annotation = params["Annotation"] + analysis_method = params["Spatial_Analysis_Method"] + # Two analysis methods available: + # 1. "Neighborhood Enrichment": Calculates how often pairs of cell types + # are neighbors compared to random chance. Positive scores indicate + # attraction/co-location, negative scores indicate avoidance. + # Output: z-scores (can be positive or negative) + # Files: neighborhood_enrichment_{identifier}.csv + # 2. "Cluster Interaction Matrix": Counts the number of edges/connections + # between different cell types in the spatial neighborhood graph. + # Shows absolute interaction frequencies rather than enrichment. + # Output: raw counts (always positive integers) + # Files: cluster_interaction_matrix_{identifier}.csv + # Both methods produce the same data structure, just different values + stratify_by = params.get("Stratify_By", ["None"]) + seed = params.get("Seed", "None") + coord_type = params.get("Coordinate_Type", "None") + n_rings = 1 + n_neighs = params.get("K_Nearest_Neighbors", 6) + radius = params.get("Radius", "None") + image_width = params.get("Figure_Width", 15) + image_height = params.get("Figure_Height", 12) + dpi = params.get("Figure_DPI", 200) + font_size = params.get("Font_Size", 12) + color_bar_range = params.get("Color_Bar_Range", "Automatic") + + def save_matrix(matrix): + for file_name in matrix: + data_df = matrix[file_name] + print("\n") + print(file_name) + print(data_df) + # In SPAC, collect matrices for later saving instead of + # direct file write + matrices[file_name] = data_df + + def update_nidap_display( + axs, + image_width, + image_height, + dpi, + font_size + ): + # NIDAP display logic is different than the generic python + # image output. For example, a 12in*8in image with font 12 + # should properly display all text in generic Image + # But in nidap code workbook resizing, the text will be reducted + # This function is to adjust the image sizing and font sizing + # to fit the NIDAP display + # Get the figure associated with the axes + fig = axs.get_figure() + + # Set figure size and DPI + fig.set_size_inches(image_width, image_height) + fig.set_dpi(dpi) + + # Customize font sizes + axs.title.set_fontsize(font_size) # Title font size + axs.xaxis.label.set_fontsize(font_size) # X-axis label font size + axs.yaxis.label.set_fontsize(font_size) # Y-axis label font size + axs.tick_params(axis='both', labelsize=font_size) # Tick labels + # Return the updated figure and axes for chaining or further use + # Note: This adjustment was specific to NIDAP display resizing + # behavior and may not be necessary in other environments + return fig, axs + + for i, item in enumerate(stratify_by): + item_is_none = text_to_value(item) + if item_is_none is None and i == 0: + stratify_by = item_is_none + elif item_is_none is None and i != 0: + raise ValueError( + 'Found string "None" in the stratify by list that is ' + 'not the first entry.\n' + 'Please remove the "None" to proceed with the list of ' + 'stratify by options, \n' + 'or move the "None" to start of the list to disable ' + 'stratification. Thank you.') + + seed = text_to_value(seed, to_int=True) + radius = text_to_value(radius, to_float=True) + coord_type = text_to_value(coord_type) + color_bar_range = text_to_value( + color_bar_range, + "Automatic", + to_float=True) + + if color_bar_range is not None: + cmap = "seismic" + vmin = -abs(color_bar_range) + vmax = abs(color_bar_range) + else: + cmap = "seismic" + vmin = vmax = color_bar_range + + plt.rcParams['font.size'] = font_size + + result_dictionary = spatial_interaction( + adata=adata, + annotation=annotation, + analysis_method=analysis_method, + stratify_by=stratify_by, + return_matrix=True, + seed=seed, + coord_type=coord_type, + n_rings=n_rings, + n_neighs=n_neighs, + radius=radius, + cmap=cmap, + vmin=vmin, + vmax=vmax, + figsize=(image_width, image_height), + dpi=dpi + ) + + # Track figures and matrices for optional saving + figures = [] + matrices = {} + + if not stratify_by: + axs = result_dictionary['Ax'] + fig, axs = update_nidap_display( + axs=axs, + image_width=image_width, + image_height=image_height, + dpi=dpi, + font_size=font_size + ) + figures.append(fig) + if show_plot: + plt.show() + + matrix = result_dictionary['Matrix']['annotation'] + save_matrix(matrix) + else: + plt.close(1) + axs_dict = result_dictionary['Ax'] + for key in axs_dict: + axs = axs_dict[key] + fig, axs = update_nidap_display( + axs=axs, + image_width=image_width, + image_height=image_height, + dpi=dpi, + font_size=font_size + ) + figures.append(fig) + if show_plot: + plt.show() + + matrix_dict = result_dictionary['Matrix'] + for identifier in matrix_dict: + matrix = matrix_dict[identifier] + save_matrix(matrix) + + # Handle saving if requested (separate from NIDAP logic) + if save_results and (figures or matrices): + saved_files = {} + output_prefix = params.get("Output_File", "spatial_interaction") + + # Save figures + if figures: + if len(figures) == 1: + output_file = f"{output_prefix}.png" + figures[0].savefig( + output_file, dpi=dpi, bbox_inches='tight') + saved_files[output_file] = output_file + else: + for i, fig in enumerate(figures): + output_file = f"{output_prefix}_plot_{i+1}.png" + fig.savefig( + output_file, dpi=dpi, bbox_inches='tight') + saved_files[output_file] = output_file + + # Save matrices + for file_name, df in matrices.items(): + saved_files.update(save_outputs({file_name: df})) + + # Close figures after saving + for fig in figures: + plt.close(fig) + + print( + f"Spatial Interaction completed → " + f"{list(saved_files.keys())}" + ) + return saved_files + + return None + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python spatial_interaction_template.py " + "", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned data object") \ No newline at end of file diff --git a/tests/templates/test_spatial_interaction_template.py b/tests/templates/test_spatial_interaction_template.py new file mode 100644 index 00000000..992cbc9a --- /dev/null +++ b/tests/templates/test_spatial_interaction_template.py @@ -0,0 +1,242 @@ +# tests/templates/test_spatial_interaction_template.py +"""Unit tests for the Spatial Interaction template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import matplotlib +matplotlib.use("Agg") # Headless backend for CI + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.spatial_interaction_template import run_from_json + + +def mock_adata(n_cells: int = 100) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "cell_type": ( + ["TypeA", "TypeB", "TypeC"] * ((n_cells + 2) // 3) + )[:n_cells], + "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells], + "batch": (["Batch1", "Batch2"] * ((n_cells + 1) // 2))[:n_cells] + }) + x_mat = rng.normal(size=(n_cells, 5)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = [f"Marker{i}" for i in range(5)] + # Add spatial coordinates - required for spatial analysis + adata.obsm["spatial"] = rng.random((n_cells, 2)) * 100 + return adata + + +class TestSpatialInteractionTemplate(unittest.TestCase): + """Unit tests for the Spatial Interaction template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "spatial_interaction" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters from NIDAP template + self.params = { + "Upstream_Analysis": self.in_file, + "Annotation": "cell_type", + "Spatial_Analysis_Method": "Neighborhood Enrichment", + "Stratify_By": ["None"], + "Coordinate_Type": "None", + "Seed": "None", + "Radius": "None", + "K_Nearest_Neighbors": 6, + "Figure_Width": 15, + "Figure_Height": 12, + "Figure_DPI": 200, + "Font_Size": 12, + "Color_Bar_Range": "Automatic", + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + @patch('spac.templates.spatial_interaction_template.' + 'spatial_interaction') + def test_complete_io_workflow(self, mock_spatial) -> None: + """Single I/O test covering input/output scenarios.""" + # Mock the spatial_interaction function + mock_fig = MagicMock() + mock_fig.number = 1 + mock_ax = MagicMock() + mock_ax.get_figure.return_value = mock_fig + mock_ax.title = MagicMock() + mock_ax.xaxis = MagicMock() + mock_ax.yaxis = MagicMock() + mock_ax.xaxis.label = MagicMock() + mock_ax.yaxis.label = MagicMock() + mock_ax.tick_params = MagicMock() + + # Mock matrix data matching expected output + mock_matrix = { + 'neighborhood_enrichment.csv': pd.DataFrame({ + 'TypeA': [1.0, 0.5, 0.3], + 'TypeB': [0.5, 1.0, 0.7], + 'TypeC': [0.3, 0.7, 1.0] + }, index=['TypeA', 'TypeB', 'TypeC']) + } + + mock_spatial.return_value = { + 'Ax': mock_ax, + 'Matrix': {'annotation': mock_matrix} + } + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters + result = run_from_json(self.params, show_plot=False) + self.assertIsInstance(result, dict) + # Verify both PNG and CSV files were saved + self.assertTrue(len(result) >= 2) # At least 1 PNG + 1 CSV + png_files = [f for f in result.keys() if f.endswith('.png')] + csv_files = [f for f in result.keys() if f.endswith('.csv')] + self.assertEqual(len(png_files), 1) + self.assertEqual(len(csv_files), 1) + + # Test 2: Run without saving + result_no_save = run_from_json( + self.params, save_results=False, show_plot=False + ) + # For multi-plot template, returns None when not saving + self.assertIsNone(result_no_save) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path, show_plot=False) + self.assertIsInstance(result_json, dict) + self.assertTrue(len(result_json) >= 2) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test invalid stratify_by with None not at first position + params_bad = self.params.copy() + params_bad["Stratify_By"] = ["sample", "None", "batch"] + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad, show_plot=False) + + # Check exact error message + expected_msg = ( + 'Found string "None" in the stratify by list that is ' + 'not the first entry.\n' + 'Please remove the "None" to proceed with the list of ' + 'stratify by options, \n' + 'or move the "None" to start of the list to disable ' + 'stratification. Thank you.' + ) + self.assertEqual(str(context.exception), expected_msg) + + @patch('spac.templates.spatial_interaction_template.' + 'spatial_interaction') + def test_function_calls(self, mock_spatial) -> None: + """Test that main function is called with correct parameters.""" + # Mock the spatial_interaction function with stratified output + mock_fig1 = MagicMock() + mock_fig1.number = 1 + mock_fig2 = MagicMock() + mock_fig2.number = 2 + + mock_ax1 = MagicMock() + mock_ax1.get_figure.return_value = mock_fig1 + mock_ax1.title = MagicMock() + mock_ax1.xaxis = MagicMock() + mock_ax1.yaxis = MagicMock() + mock_ax1.xaxis.label = MagicMock() + mock_ax1.yaxis.label = MagicMock() + mock_ax1.tick_params = MagicMock() + + mock_ax2 = MagicMock() + mock_ax2.get_figure.return_value = mock_fig2 + mock_ax2.title = MagicMock() + mock_ax2.xaxis = MagicMock() + mock_ax2.yaxis = MagicMock() + mock_ax2.xaxis.label = MagicMock() + mock_ax2.yaxis.label = MagicMock() + mock_ax2.tick_params = MagicMock() + + # Test with stratification - should produce multiple outputs + mock_spatial.return_value = { + 'Ax': {'sample_S1': mock_ax1, 'sample_S2': mock_ax2}, + 'Matrix': { + 'sample_S1': { + 'S1_enrichment.csv': pd.DataFrame({'A': [1, 2]}) + }, + 'sample_S2': { + 'S2_enrichment.csv': pd.DataFrame({'B': [3, 4]}) + } + } + } + + params_stratified = self.params.copy() + params_stratified["Stratify_By"] = ["sample"] + params_stratified["Seed"] = "42" + params_stratified["Radius"] = "50.0" + params_stratified["Color_Bar_Range"] = "2.5" + params_stratified["Spatial_Analysis_Method"] = ( + "Cluster Interaction Matrix" + ) + + result = run_from_json(params_stratified, show_plot=False) + + # Verify function was called correctly + mock_spatial.assert_called_once() + call_args = mock_spatial.call_args + + # Check specific parameter conversions + self.assertEqual(call_args[1]['annotation'], "cell_type") + self.assertEqual( + call_args[1]['analysis_method'], + "Cluster Interaction Matrix" + ) + self.assertEqual(call_args[1]['stratify_by'], ["sample"]) + self.assertEqual(call_args[1]['seed'], 42) + self.assertEqual(call_args[1]['radius'], 50.0) + self.assertEqual(call_args[1]['n_neighs'], 6) + self.assertEqual(call_args[1]['vmin'], -2.5) + self.assertEqual(call_args[1]['vmax'], 2.5) + self.assertEqual(call_args[1]['cmap'], "seismic") + + # Verify multiple files were saved (2 plots + 2 matrices) + self.assertIsInstance(result, dict) + self.assertEqual(len(result), 4) + png_files = [f for f in result.keys() if f.endswith('.png')] + csv_files = [f for f in result.keys() if f.endswith('.csv')] + self.assertEqual(len(png_files), 2) + self.assertEqual(len(csv_files), 2) + + + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 1a3d03daa6e0630132c2596e149a74cdba0520a0 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sun, 3 Aug 2025 21:13:09 -0400 Subject: [PATCH 059/102] fix(spatial_interaction_template): fix typo --- src/spac/templates/spatial_interaction_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spac/templates/spatial_interaction_template.py b/src/spac/templates/spatial_interaction_template.py index 84d5c496..85e5fc88 100644 --- a/src/spac/templates/spatial_interaction_template.py +++ b/src/spac/templates/spatial_interaction_template.py @@ -108,7 +108,7 @@ def update_nidap_display( # NIDAP display logic is different than the generic python # image output. For example, a 12in*8in image with font 12 # should properly display all text in generic Image - # But in nidap code workbook resizing, the text will be reducted + # But in nidap code workbook resizing, the text will be reduced. # This function is to adjust the image sizing and font sizing # to fit the NIDAP display # Get the figure associated with the axes From 19cd477fbb3af575ae2b90615b34e801fb4dc66c Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sun, 3 Aug 2025 21:46:15 -0400 Subject: [PATCH 060/102] feat(nearest_neighbor_calculation_template): add nearest_neighbor_calculation_template function and unit tests --- .../nearest_neighbor_calculation_template.py | 138 +++++++++++++++ ...t_nearest_neighbor_calculation_template.py | 162 ++++++++++++++++++ 2 files changed, 300 insertions(+) create mode 100644 src/spac/templates/nearest_neighbor_calculation_template.py create mode 100644 tests/templates/test_nearest_neighbor_calculation_template.py diff --git a/src/spac/templates/nearest_neighbor_calculation_template.py b/src/spac/templates/nearest_neighbor_calculation_template.py new file mode 100644 index 00000000..57417b1a --- /dev/null +++ b/src/spac/templates/nearest_neighbor_calculation_template.py @@ -0,0 +1,138 @@ +""" +Platform-agnostic Nearest Neighbor Calculation template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.nearest_neighbor_calculation_template import ( +... run_from_json +... ) +>>> run_from_json("examples/nearest_neighbor_calculation_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union +import pandas as pd + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.utils import check_table, check_annotation +from spac.spatial_analysis import calculate_nearest_neighbor +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], Any]: + """ + Execute Nearest Neighbor Calculation analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the adata object + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or AnnData + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed AnnData object + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + annotation = params["Annotation"] + spatial_associated_table = "spatial" + imageid = params.get("ImageID", "None") + label = params.get( + "Nearest_Neighbor_Associated_Table", "spatial_distance" + ) + verbose = params.get("Verbose", True) + + # Convert any string "None" to actual None for Python + imageid = text_to_value(imageid, default_none_text="None") + + print( + "Running `calculate_nearest_neighbor` with the following parameters:" + ) + print(f" annotation: {annotation}") + print(f" spatial_associated_table: {spatial_associated_table}") + print(f" imageid: {imageid}") + print(f" label: {label}") + print(f" verbose: {verbose}") + + # Perform the nearest neighbor calculation + calculate_nearest_neighbor( + adata=adata, + annotation=annotation, + spatial_associated_table=spatial_associated_table, + imageid=imageid, + label=label, + verbose=verbose + ) + + print("Nearest neighbor calculation complete.") + print("adata.obsm keys:", list(adata.obsm.keys())) + if label in adata.obsm: + print( + f"Preview of adata.obsm['{label}']:\n", + adata.obsm[label].head() + ) + + print(adata) + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "transform_output.pickle") + # Default to pickle format if no recognized extension + if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): + output_file = output_file + '.pickle' + + saved_files = save_outputs({output_file: adata}) + + print( + f"Nearest Neighbor Calculation completed → " + f"{saved_files[output_file]}" + ) + return saved_files + else: + # Return the adata object directly for in-memory workflows + print("Returning AnnData object (not saving to file)") + return adata + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python nearest_neighbor_calculation_template.py " + "", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned AnnData object") \ No newline at end of file diff --git a/tests/templates/test_nearest_neighbor_calculation_template.py b/tests/templates/test_nearest_neighbor_calculation_template.py new file mode 100644 index 00000000..c0e3f894 --- /dev/null +++ b/tests/templates/test_nearest_neighbor_calculation_template.py @@ -0,0 +1,162 @@ +# tests/templates/test_nearest_neighbor_calculation_template.py +"""Unit tests for the Nearest Neighbor Calculation template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.nearest_neighbor_calculation_template import ( + run_from_json +) + + +def mock_adata(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], + "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] + }) + x_mat = rng.normal(size=(n_cells, 3)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = ["Gene1", "Gene2", "Gene3"] + # Add spatial coordinates + adata.obsm["spatial"] = rng.random((n_cells, 2)) * 100 + return adata + + +class TestNearestNeighborCalculationTemplate(unittest.TestCase): + """Unit tests for the Nearest Neighbor Calculation template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "output" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters - from NIDAP template + self.params = { + "Upstream_Analysis": self.in_file, + "Annotation": "cell_type", + "ImageID": "None", + "Nearest_Neighbor_Associated_Table": "spatial_distance", + "Verbose": True, + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Mock the calculate_nearest_neighbor function + with patch( + 'spac.templates.nearest_neighbor_calculation_template.' + 'calculate_nearest_neighbor' + ) as mock_calc_nn: + # Mock function adds a distance matrix to adata.obsm + def side_effect(adata, annotation, spatial_associated_table, + imageid, label, verbose): + # Simulate adding distance matrix to obsm + n_cells = adata.n_obs + unique_types = adata.obs[annotation].unique() + n_types = len(unique_types) + # Create mock distance dataframe + dist_df = pd.DataFrame( + np.random.rand(n_cells, n_types), + columns=unique_types, + index=adata.obs.index + ) + adata.obsm[label] = dist_df + + mock_calc_nn.side_effect = side_effect + + # Test 1: Run with default parameters + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + # Verify file was saved + self.assertTrue(len(result) > 0) + + # Test 2: Run without saving + result_no_save = run_from_json( + self.params, save_results=False + ) + # Check appropriate return type + self.assertIsInstance(result_no_save, ad.AnnData) + # Verify nearest neighbor distances were added + self.assertIn("spatial_distance", result_no_save.obsm) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + def test_function_calls(self) -> None: + """Test that main function is called with correct parameters.""" + with patch( + 'spac.templates.nearest_neighbor_calculation_template.' + 'calculate_nearest_neighbor' + ) as mock_calc_nn: + # Test with different parameters + params_alt = self.params.copy() + params_alt["ImageID"] = "sample" + params_alt["Nearest_Neighbor_Associated_Table"] = "nn_distances" + params_alt["Verbose"] = False + + run_from_json(params_alt, save_results=False) + + # Verify function was called correctly + mock_calc_nn.assert_called_once() + call_args = mock_calc_nn.call_args + + # Check parameter conversions + self.assertEqual(call_args[1]['annotation'], "cell_type") + self.assertEqual( + call_args[1]['spatial_associated_table'], "spatial" + ) + self.assertEqual(call_args[1]['imageid'], "sample") + self.assertEqual(call_args[1]['label'], "nn_distances") + self.assertEqual(call_args[1]['verbose'], False) + + def test_imageid_none_conversion(self) -> None: + """Test that string 'None' is converted to actual None.""" + with patch( + 'spac.templates.nearest_neighbor_calculation_template.' + 'calculate_nearest_neighbor' + ) as mock_calc_nn: + # Test with "None" string + self.params["ImageID"] = "None" + run_from_json(self.params, save_results=False) + + # Verify imageid was converted to None + call_args = mock_calc_nn.call_args + self.assertIsNone(call_args[1]['imageid']) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 07ecdfa15c3b5c4c7f65288811306bf68cef4962 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sun, 3 Aug 2025 22:52:10 -0400 Subject: [PATCH 061/102] feat(visualize_nearest_neighbor_template): add visualize_nearest_neighbor_template function and unit tests --- .../visualize_nearest_neighbor_template.py | 429 ++++++++++++++++++ ...est_visualize_nearest_neighbor_template.py | 237 ++++++++++ 2 files changed, 666 insertions(+) create mode 100644 src/spac/templates/visualize_nearest_neighbor_template.py create mode 100644 tests/templates/test_visualize_nearest_neighbor_template.py diff --git a/src/spac/templates/visualize_nearest_neighbor_template.py b/src/spac/templates/visualize_nearest_neighbor_template.py new file mode 100644 index 00000000..e08d1af0 --- /dev/null +++ b/src/spac/templates/visualize_nearest_neighbor_template.py @@ -0,0 +1,429 @@ +""" +Platform-agnostic Visualize Nearest Neighbor template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.visualize_nearest_neighbor_template import ( +... run_from_json +... ) +>>> run_from_json("examples/visualize_nearest_neighbor_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, Tuple, List +import pandas as pd +import numpy as np +from matplotlib.axes import Axes +import matplotlib.pyplot as plt +import matplotlib.patches as mpatches + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.visualization import visualize_nearest_neighbor +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True, + show_plot: bool = True +) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: + """ + Execute Visualize Nearest Neighbor analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the figure and + dataframe directly for in-memory workflows. Default is True. + show_plot : bool, optional + Whether to display the plot. Default is True. + + Returns + ------- + dict or tuple + If save_results=True: Dictionary of saved file paths + If save_results=False: Tuple of (figure(s), dataframe) + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + # Use direct dictionary access for required parameters + # Will raise KeyError if missing + annotation = params["Annotation"] + source_label = params["Source_Anchor_Cell_Label"] + + # Use .get() with defaults for optional parameters from JSON template + image_id = params.get("ImageID", "None") + method = params.get("Plot_Method", "numeric") + plot_type = params.get("Plot_Type", "boxen") + target_label = params.get("Target_Cell_Label", "All") + distance_key = params.get( + "Nearest_Neighbor_Associated_Table", "spatial_distance" + ) + log_scale = params.get("Log_Scale", False) + facet_plot = params.get("Facet_Plot", False) + x_axis_title_rotation = params.get("X_Axis_Label_Rotation", 0) + shared_x_axis_title = params.get("Shared_X_Axis_Title_", True) + x_axis_title_fontsize = params.get("X_Axis_Title_Font_Size", "None") + + defined_color_map = text_to_value( + params.get("Defined_Color_Mapping", "None"), + param_name="Define Label Color Mapping" + ) + annotation_colorscale = "rainbow" + + fig_width = params.get("Figure_Width", 12) + fig_height = params.get("Figure_Height", 6) + fig_dpi = params.get("FIgure_DPI", 300) + global_font_size = params.get("Font_Size", 12) + fig_title = ( + f'Nearest Neighbor Distance Distribution Measured from ' + f'"{source_label}"' + ) + + image_id = text_to_value( + image_id, + default_none_text="None", + value_to_convert_to=None + ) + + # If target_label is None, it means "All distance columns" + # If it's a comma-separated string (e.g. "Stroma,Immune"), + # split into a list + target_label = text_to_value( + target_label, + default_none_text="All", + value_to_convert_to=None + ) + + if target_label is not None: + distance_to_processed = [x.strip() for x in target_label.split(",")] + else: + distance_to_processed = None + + x_axis_title_fontsize = text_to_value( + x_axis_title_fontsize, + default_none_text="None", + to_int="True" + ) + + # Configure Matplotlib font size + plt.rcParams.update({'font.size': global_font_size}) + + # If facet_plot=True but no valid stratify column => revert to + # single figure + if facet_plot and image_id is None: + warning_message = ( + "Facet plotting was requested, but there is no annotation " + "to group by. Switching to a single-figure display." + ) + print(warning_message) + facet_plot = False + + result_dict = visualize_nearest_neighbor( + adata=adata, + annotation=annotation, + spatial_distance=distance_key, + distance_from=source_label, + distance_to=distance_to_processed, + method=method, + plot_type=plot_type, + stratify_by=image_id, + facet_plot=facet_plot, + log=log_scale, + annotation_colorscale=annotation_colorscale, + defined_color_map=defined_color_map, + ) + + # Extract the data and figure(s) + df_long = result_dict["data"] + figs_out = result_dict["fig"] # Single Figure or List of Figures + palette_hex = result_dict["palette"] + axes_out = result_dict["ax"] + + print("Summary statistics of the dataset:") + print(df_long.describe()) + + # Customize figure legends & X-axis rotation + legend_labels = ( + distance_to_processed or df_long["group"].unique().tolist() + ) + legend_labels = ( + legend_labels if distance_to_processed else sorted(legend_labels) + ) + + handles = [ + mpatches.Patch( + facecolor=palette_hex[label], + edgecolor='none', + label=label + ) + for label in legend_labels + ] + + def _flatten_axes(ax_input): + if isinstance(ax_input, Axes): + return [ax_input] + if isinstance(ax_input, (list, tuple, np.ndarray)): + return [ + ax for ax in np.ravel(ax_input) if isinstance(ax, Axes) + ] + return [] + + flat_axes_list = _flatten_axes(axes_out) + shared_x_title_applied_to_fig = None + + if flat_axes_list: + # Attach legend to the last axis + flat_axes_list[-1].legend( + handles=handles, + title="Target phenotype", + bbox_to_anchor=(1.02, 1), + loc="upper left", + frameon=False, + ) + + # X-Axis Title Handling + current_x_label_text = "" + if flat_axes_list[0].get_xlabel(): + current_x_label_text = flat_axes_list[0].get_xlabel() + + if not current_x_label_text: + current_x_label_text = ( + f"Log({distance_key})" if log_scale else distance_key + ) + if not current_x_label_text: + current_x_label_text = "Distance" # Ultimate fallback + + effective_fontsize = ( + x_axis_title_fontsize if x_axis_title_fontsize is not None + else global_font_size + ) + + if (facet_plot and shared_x_axis_title and + isinstance(figs_out, plt.Figure)): + for ax_item in flat_axes_list: + ax_item.set_xlabel('') + + sup_ha_align = 'center' + if 0 < x_axis_title_rotation % 360 < 180: + sup_ha_align = 'right' + elif 180 < x_axis_title_rotation % 360 < 360: + sup_ha_align = 'left' + + figs_out.supxlabel( + current_x_label_text, y=0.02, fontsize=effective_fontsize, + rotation=x_axis_title_rotation, ha=sup_ha_align + ) + shared_x_title_applied_to_fig = figs_out + + else: # Apply to individual subplot x-axis titles + for ax_item in flat_axes_list: + label_object = ax_item.xaxis.get_label() + if not label_object.get_text(): # If no label, set it + ax_item.set_xlabel(current_x_label_text) + label_object = ax_item.xaxis.get_label() + + if label_object.get_text(): # Configure if actual label + label_object.set_rotation(x_axis_title_rotation) + label_object.set_fontsize(effective_fontsize) + ha_align_val = 'center' + if 0 < x_axis_title_rotation % 360 < 180: + ha_align_val = 'right' + elif 180 < x_axis_title_rotation % 360 < 360: + ha_align_val = 'left' + label_object.set_ha(ha_align_val) + + # Stratification Info + if image_id is not None and image_id in df_long.columns: + unique_vals = df_long[image_id].unique() + n_unique = len(unique_vals) + + if n_unique == 0: + print( + f"[WARNING] The annotation '{image_id}' has 0 unique " + f"values or is empty. No data to plot => Potential " + f"empty plot." + ) + elif n_unique == 1 and facet_plot: + print( + f"[INFO] The annotation '{image_id}' has only one unique " + f"value ({unique_vals[0]}). Facet plot will resemble a " + f"single plot." + ) + elif n_unique > 1: + print( + f"The annotation '{image_id}' has {n_unique} unique " + f"values: {unique_vals}" + ) + + # Figure Configuration & Display + def _title_main(fig, title): + """ + Sets a bold, centered main title on the figure, and + adjusts figure size and layout accordingly. + """ + fig.set_size_inches(fig_width, fig_height) + fig.set_dpi(fig_dpi) + fig.suptitle( + title, + fontsize=global_font_size + 4, + weight='bold', + x=0.5, # center horizontally + horizontalalignment='center' + ) + + def _label_each_figure(fig_list, categories): + """ + Adds a title to each figure, typically used when multiple + separate figures are returned (one per category). + """ + for fig, cat in zip(fig_list, categories): + if fig: + _title_main(fig, f"{fig_title}\n{image_id}: {cat}") + # Adjust top for the suptitle + fig.tight_layout(rect=[0.01, 0.01, 0.99, 0.96]) + if show_plot: + plt.show() + + # Determine the actual distance column name used in df_long for summary + distance_col = ( + "log_distance" if "log_distance" in df_long.columns else "distance" + ) + + # Displaying Figures + cat_list = [] + if image_id and (image_id in df_long.columns): + if pd.api.types.is_categorical_dtype(df_long[image_id]): + cat_list = list(df_long[image_id].cat.categories) + else: + cat_list = df_long[image_id].unique().tolist() + + # Track figures for optional saving + figures = [] + + if isinstance(figs_out, list) and not facet_plot and \ + cat_list and len(figs_out) == len(cat_list): + # Scenario: Multiple separate figures, one per category + # (non-faceted) + figures = figs_out + _label_each_figure(figs_out, cat_list) + if show_plot: + plt.show() + else: + # Scenario: Single figure (faceted) or list of figures not + # matching categories + figures_to_display = ( + figs_out if isinstance(figs_out, list) else [figs_out] + ) + figures = figures_to_display + for fig_item_to_display in figures_to_display: + if fig_item_to_display is not None: + _title_main(fig_item_to_display, fig_title) + + bottom_padding = 0.01 + # Make space for shared x-title + if fig_item_to_display is shared_x_title_applied_to_fig: + bottom_padding = 0.01 # Adjusted from 0.05 + + top_padding = 0.99 # Adjusted from 0.90 + + # rect=[left, bottom, right, top] + fig_item_to_display.tight_layout( + rect=[0.01, bottom_padding, 0.99, top_padding] + ) + if show_plot: + plt.show() + + # summary statistics + # 1) Per-group summary + df_summary_group = ( + df_long + .groupby("group")[distance_col] + .describe() + .reset_index() + ) + + # 2) Per-group-and-stratify, if image_id is valid + if image_id and (image_id in df_long.columns): + df_summary_group_strat = ( + df_long + .groupby([image_id, "group"])[distance_col] + .describe() + .reset_index() + ) + else: + df_summary_group_strat = None + + if df_summary_group_strat is not None: + print(f"\nSummary by group(target phenotypes) AND '{image_id}':") + print(df_summary_group_strat) + else: + print("\nSummary: By group(target phenotypes) only") + print(df_summary_group) + + # CSV Output + final_df = ( + df_summary_group_strat if df_summary_group_strat is not None + else df_summary_group + ) + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get( + "Output_File", "nearest_neighbor_plots.csv" + ) + saved_files = save_outputs({output_file: final_df}) + + print(f"\nSaved summary statistics to '{output_file}'.") + print( + f"Visualize Nearest Neighbor completed → " + f"{saved_files[output_file]}" + ) + return saved_files + else: + # Return the figure(s) and dataframe directly for in-memory + # workflows + print("Returning figure(s) and dataframe (not saving to file)") + # If single figure, return it directly; if multiple, return list + if len(figures) == 1: + return figures[0], final_df + else: + return figures, final_df + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python visualize_nearest_neighbor_template.py " + "", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned figure(s) and dataframe") \ No newline at end of file diff --git a/tests/templates/test_visualize_nearest_neighbor_template.py b/tests/templates/test_visualize_nearest_neighbor_template.py new file mode 100644 index 00000000..14b641f6 --- /dev/null +++ b/tests/templates/test_visualize_nearest_neighbor_template.py @@ -0,0 +1,237 @@ +# tests/templates/test_visualize_nearest_neighbor_template.py +"""Unit tests for the Visualize Nearest Neighbor template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import matplotlib +matplotlib.use("Agg") # Headless backend for CI + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.visualize_nearest_neighbor_template import run_from_json + + +def mock_adata(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "renamed_phenotypes": (["Tfh", "B_cells"] * ((n_cells + 1) // 2))[:n_cells], + "image_id": (["Image1", "Image2"] * ((n_cells + 1) // 2))[:n_cells] + }) + x_mat = rng.normal(size=(n_cells, 3)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = ["Marker1", "Marker2", "Marker3"] + + # Add spatial coordinates + adata.obsm["spatial"] = rng.random((n_cells, 2)) * 100 + + # Add mock distance matrix with proper structure + # Create a mock distance matrix in obsm + # For nearest neighbor, we need distances from each phenotype + unique_phenotypes = obs["renamed_phenotypes"].unique() + n_phenotypes = len(unique_phenotypes) + + # Create random distance matrix + distance_matrix = rng.exponential(scale=10, size=(n_cells, n_phenotypes)) + distance_df = pd.DataFrame( + distance_matrix, + columns=[f"distance_to_{pheno}" for pheno in unique_phenotypes], + index=adata.obs.index # Use the same index as adata.obs + ) + adata.obsm["spatial_distance"] = distance_df + + return adata + + +class TestVisualizeNearestNeighborTemplate(unittest.TestCase): + """Unit tests for the Visualize Nearest Neighbor template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "nearest_neighbor_plots.csv" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters - adjust based on template + self.params = { + "Upstream_Analysis": self.in_file, + "Annotation": "renamed_phenotypes", + "ImageID": "None", + "Plot_Method": "numeric", + "Plot_Type": "boxen", + "Source_Anchor_Cell_Label": "Tfh", + "Target_Cell_Label": "All", + "Nearest_Neighbor_Associated_Table": "spatial_distance", + "Log_Scale": False, + "Facet_Plot": False, + "X_Axis_Label_Rotation": 0, + "Shared_X_Axis_Title_": True, + "X_Axis_Title_Font_Size": "None", + "Defined_Color_Mapping": "None", + "Figure_Width": 12, + "Figure_Height": 6, + "FIgure_DPI": 300, + "Font_Size": 12, + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + @patch('spac.templates.visualize_nearest_neighbor_template.' + 'visualize_nearest_neighbor') + def test_complete_io_workflow(self, mock_vis_nn) -> None: + """Single I/O test covering input/output scenarios.""" + # Mock the visualize_nearest_neighbor function + mock_fig = MagicMock() + mock_fig.number = 1 + mock_fig.set_size_inches = MagicMock() + mock_fig.set_dpi = MagicMock() + mock_fig.suptitle = MagicMock() + mock_fig.tight_layout = MagicMock() + mock_fig.supxlabel = MagicMock() + + mock_ax = MagicMock() + mock_ax.get_legend.return_value = None + mock_ax.get_xlabel.return_value = "distance" + mock_ax.set_xlabel = MagicMock() + mock_ax.xaxis.get_label.return_value = MagicMock() + + # Create mock dataframe with expected columns + mock_df = pd.DataFrame({ + 'group': ['B_cells', 'Tfh'] * 5, + 'distance': np.random.rand(10) * 10, + 'image_id': ['Image1'] * 5 + ['Image2'] * 5 + }) + + # Mock palette + mock_palette = { + 'B_cells': '#1f77b4', + 'Tfh': '#ff7f0e' + } + + mock_vis_nn.return_value = { + "data": mock_df, + "fig": mock_fig, + "palette": mock_palette, + "ax": mock_ax + } + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters + result = run_from_json(self.params, show_plot=False) + self.assertIsInstance(result, dict) + self.assertIn(self.out_file, result) + + # Test 2: Run without saving + result_no_save = run_from_json( + self.params, save_results=False, show_plot=False + ) + # Check appropriate return type - should be tuple + self.assertIsInstance(result_no_save, tuple) + self.assertEqual(len(result_no_save), 2) + # First element is figure, second is dataframe + self.assertIsInstance(result_no_save[1], pd.DataFrame) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path, show_plot=False) + self.assertIsInstance(result_json, dict) + + # Verify visualize_nearest_neighbor was called correctly + mock_vis_nn.assert_called() + call_args = mock_vis_nn.call_args[1] + self.assertEqual(call_args['annotation'], "renamed_phenotypes") + self.assertEqual(call_args['spatial_distance'], "spatial_distance") + self.assertEqual(call_args['distance_from'], "Tfh") + self.assertEqual(call_args['distance_to'], None) # "All" → None + self.assertEqual(call_args['method'], "numeric") + self.assertEqual(call_args['plot_type'], "boxen") + self.assertEqual(call_args['log'], False) + self.assertEqual(call_args['facet_plot'], False) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test invalid integer conversion for x_axis_title_fontsize + params_bad = self.params.copy() + params_bad["X_Axis_Title_Font_Size"] = "invalid_size" + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + # Check exact error message from text_to_value + expected_msg = ( + "Error: can't convert to integer. " + "Received:\"invalid_size\"" + ) + self.assertEqual(str(context.exception), expected_msg) + + @patch('spac.templates.visualize_nearest_neighbor_template.' + 'visualize_nearest_neighbor') + def test_function_calls(self, mock_vis_nn) -> None: + """Test that main function is called with correct parameters.""" + # Mock the main function + mock_fig = MagicMock() + mock_ax = MagicMock() + mock_ax.get_legend.return_value = None + mock_ax.get_xlabel.return_value = "" + + mock_df = pd.DataFrame({ + 'group': ['B_cells', 'Tfh', 'Stroma'] * 2, + 'distance': [5.0, 7.0, 3.0, 6.0, 8.0, 4.0] + }) + + mock_vis_nn.return_value = { + "data": mock_df, + "fig": mock_fig, + "palette": {'B_cells': '#000', 'Tfh': '#fff', 'Stroma': '#ccc'}, + "ax": mock_ax + } + + # Test with different parameters + params_alt = self.params.copy() + params_alt["Target_Cell_Label"] = "B_cells,Stroma" + params_alt["Log_Scale"] = True + params_alt["Facet_Plot"] = True + params_alt["ImageID"] = "image_id" + params_alt["X_Axis_Label_Rotation"] = 45 + + run_from_json(params_alt, save_results=False, show_plot=False) + + # Verify function was called correctly + mock_vis_nn.assert_called_once() + call_args = mock_vis_nn.call_args[1] + + # Check specific parameter conversions + self.assertEqual(call_args['distance_to'], ["B_cells", "Stroma"]) + self.assertEqual(call_args['log'], True) + self.assertEqual(call_args['facet_plot'], True) + self.assertEqual(call_args['stratify_by'], "image_id") + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 52a4ee6ef66f9ffaed59391bbe6d0fd4f003a816 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Sun, 3 Aug 2025 23:06:03 -0400 Subject: [PATCH 062/102] fix(visualize_nearest_neighbor_template): fix typo --- src/spac/templates/visualize_nearest_neighbor_template.py | 2 +- tests/templates/test_visualize_nearest_neighbor_template.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spac/templates/visualize_nearest_neighbor_template.py b/src/spac/templates/visualize_nearest_neighbor_template.py index e08d1af0..c5719509 100644 --- a/src/spac/templates/visualize_nearest_neighbor_template.py +++ b/src/spac/templates/visualize_nearest_neighbor_template.py @@ -90,7 +90,7 @@ def run_from_json( fig_width = params.get("Figure_Width", 12) fig_height = params.get("Figure_Height", 6) - fig_dpi = params.get("FIgure_DPI", 300) + fig_dpi = params.get("Figure_DPI", 300) global_font_size = params.get("Font_Size", 12) fig_title = ( f'Nearest Neighbor Distance Distribution Measured from ' diff --git a/tests/templates/test_visualize_nearest_neighbor_template.py b/tests/templates/test_visualize_nearest_neighbor_template.py index 14b641f6..429dffd2 100644 --- a/tests/templates/test_visualize_nearest_neighbor_template.py +++ b/tests/templates/test_visualize_nearest_neighbor_template.py @@ -89,7 +89,7 @@ def setUp(self) -> None: "Defined_Color_Mapping": "None", "Figure_Width": 12, "Figure_Height": 6, - "FIgure_DPI": 300, + "Figure_DPI": 300, "Font_Size": 12, "Output_File": self.out_file, } From 6ab7a9d91111c6fd1aa4a771bf85f8d07bd01b28 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 00:03:52 -0400 Subject: [PATCH 063/102] feat(template_utils): add string_list_to_dictionary to template utils --- src/spac/templates/template_utils.py | 89 +++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/src/spac/templates/template_utils.py b/src/spac/templates/template_utils.py index de42ebaa..48d45abf 100644 --- a/src/spac/templates/template_utils.py +++ b/src/spac/templates/template_utils.py @@ -618,4 +618,91 @@ def clean_column_name(column_name): print("\n\nFinal Dataframe Info") print(final_df.info()) - return final_df \ No newline at end of file + return final_df + + +def string_list_to_dictionary( + input_list: List[str], + key_name: str = "key", + value_name: str = "color" +) -> Dict[str, str]: + """ + Validate that a list contains strings in the "key:value" format + and return the parsed dictionary. Reports all invalid entries with + custom key and value names in error messages. + + Parameters + ---------- + input_list : list + List of strings to validate and parse + key_name : str, optional + Name to describe the 'key' part in error messages. Default is "key" + value_name : str, optional + Name to describe the 'value' part in error messages. Default is "color" + + Returns + ------- + dict + A dictionary parsed from the input list if all entries are valid + + Raises + ------ + TypeError + If input is not a list + ValueError + If any entry in the list is not a valid "key:value" format + + Examples + -------- + >>> string_list_to_dictionary(["red:#FF0000", "blue:#0000FF"]) + {'red': '#FF0000', 'blue': '#0000FF'} + + >>> string_list_to_dictionary(["TypeA:Cancer", "TypeB:Normal"], "cell_type", "diagnosis") + {'TypeA': 'Cancer', 'TypeB': 'Normal'} + """ + if not isinstance(input_list, list): + raise TypeError("Input must be a list.") + + parsed_dict = {} + errors = [] + seen_keys = set() + + for entry in input_list: + if not isinstance(entry, str): + errors.append( + f"\nInvalid entry '{entry}': Must be a string in the " + f"'{key_name}:{value_name}' format." + ) + continue + if ":" not in entry: + errors.append( + f"\nInvalid entry '{entry}': Missing ':' separator to " + f"separate '{key_name}' and '{value_name}'." + ) + continue + + key, *value = map(str.strip, entry.split(":", 1)) + if not key or not value: + errors.append( + f"\nInvalid entry '{entry}': Both '{key_name}' and " + f"'{value_name}' must be non-empty." + ) + continue + + if key in seen_keys: + errors.append(f"\nDuplicate {key_name} '{key}' found.") + else: + seen_keys.add(key) + parsed_dict[key] = value[0] + + # Add to dictionary if valid + parsed_dict[key] = value[0] + + # Raise error if there are invalid entries + if errors: + raise ValueError( + "\nValidation failed for the following entries:\n" + + "\n".join(errors) + ) + + return parsed_dict \ No newline at end of file From 2477266b7de03e603b1dc9d48b9a53bed0af61ad Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 01:25:19 -0400 Subject: [PATCH 064/102] feat(add_pin_color_rule_template): add add_pin_color_rule_template fnction and unit tests --- .../templates/add_pin_color_rule_template.py | 108 +++++++++++++ tests/templates/test_add_pin_color_rule.py | 149 ++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 src/spac/templates/add_pin_color_rule_template.py create mode 100644 tests/templates/test_add_pin_color_rule.py diff --git a/src/spac/templates/add_pin_color_rule_template.py b/src/spac/templates/add_pin_color_rule_template.py new file mode 100644 index 00000000..b6899102 --- /dev/null +++ b/src/spac/templates/add_pin_color_rule_template.py @@ -0,0 +1,108 @@ +""" +Platform-agnostic Append Pin Color Rule template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.add_pin_color_rule_template import run_from_json +>>> run_from_json("examples/add_pin_color_rule_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.data_utils import add_pin_color_rules +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + string_list_to_dictionary, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], Any]: + """ + Execute Append Pin Color Rule analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the adata object + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or AnnData + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed AnnData object + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + color_dict_string_list = params.get("Label_Color_Map", []) + color_map_name = params.get("Color_Map_Name", "_spac_colors") + overwrite = params.get("Overwrite_Previous_Color_Map", True) + + color_dict = string_list_to_dictionary( + color_dict_string_list, + key_name="label", + value_name="color" + ) + + add_pin_color_rules( + adata, + label_color_dict=color_dict, + color_map_name=color_map_name, + overwrite=overwrite + ) + print(adata.uns[f'{color_map_name}_summary']) + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "color_mapped_analysis.pickle") + # Default to pickle format if no recognized extension + if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): + output_file = output_file + '.pickle' + + saved_files = save_outputs({output_file: adata}) + + print(f"Append Pin Color Rule completed → {saved_files[output_file]}") + return saved_files + else: + # Return the adata object directly for in-memory workflows + print("Returning AnnData object (not saving to file)") + return adata + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python add_pin_color_rule_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned AnnData object") \ No newline at end of file diff --git a/tests/templates/test_add_pin_color_rule.py b/tests/templates/test_add_pin_color_rule.py new file mode 100644 index 00000000..2e380deb --- /dev/null +++ b/tests/templates/test_add_pin_color_rule.py @@ -0,0 +1,149 @@ +# tests/templates/test_add_pin_color_rule_template.py +"""Unit tests for the Append Pin Color Rule template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.add_pin_color_rule_template import run_from_json + + +def mock_adata(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], + "batch": (["Batch1", "Batch2"] * ((n_cells + 1) // 2))[:n_cells] + }) + x_mat = rng.normal(size=(n_cells, 3)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = ["Gene1", "Gene2", "Gene3"] + # Initialize uns dict for color rules + adata.uns = {} + return adata + + +class TestAddPinColorRuleTemplate(unittest.TestCase): + """Unit tests for the Append Pin Color Rule template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "color_mapped_analysis" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters from NIDAP template + self.params = { + "Upstream_Analysis": self.in_file, + "Label_Color_Map": ["TypeA:red", "TypeB:blue"], + "Color_Map_Name": "_spac_colors", + "Overwrite_Previous_Color_Map": True, + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + # Verify file was saved + self.assertTrue(len(result) > 0) + pickle_files = [f for f in result.values() if '.pickle' in str(f)] + self.assertTrue(len(pickle_files) > 0) + + # Test 2: Run without saving + result_no_save = run_from_json(self.params, save_results=False) + # Check appropriate return type based on template + self.assertIsInstance(result_no_save, ad.AnnData) + # Verify color map was added + self.assertIn("_spac_colors", result_no_save.uns) + self.assertIn("_spac_colors_summary", result_no_save.uns) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test invalid color format + params_bad = self.params.copy() + params_bad["Label_Color_Map"] = ["TypeA-red"] # Missing colon + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + # Check error message contains expected text + error_msg = str(context.exception) + self.assertIn("Missing ':' separator", error_msg) + + @patch('spac.templates.add_pin_color_rule_template.add_pin_color_rules') + def test_function_calls(self, mock_add_rules) -> None: + """Test that main function is called with correct parameters.""" + # Mock the main function to simulate adding summary to adata.uns + def side_effect_add_rules(adata, **kwargs): + color_map_name = kwargs.get('color_map_name', '_spac_colors') + adata.uns[f'{color_map_name}_summary'] = "Mock summary" + return None + + mock_add_rules.side_effect = side_effect_add_rules + + # Test with different parameters + params_alt = self.params.copy() + params_alt["Color_Map_Name"] = "custom_colors" + params_alt["Overwrite_Previous_Color_Map"] = False + params_alt["Label_Color_Map"] = [ + "CD4+ T cells:cyan", + "CD8+ T cells:royalblue", + "B cells:yellowgreen" + ] + + run_from_json(params_alt, save_results=False) + + # Verify function was called correctly + mock_add_rules.assert_called_once() + call_args = mock_add_rules.call_args + + # Check specific parameter conversions + self.assertEqual(call_args[1]['color_map_name'], "custom_colors") + self.assertEqual(call_args[1]['overwrite'], False) + + # Verify color dict was parsed correctly + expected_dict = { + "CD4+ T cells": "cyan", + "CD8+ T cells": "royalblue", + "B cells": "yellowgreen" + } + self.assertEqual(call_args[1]['label_color_dict'], expected_dict) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 5e68e02faaa423f381d3d7321c1378f51e7f3c7f Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 02:34:54 -0400 Subject: [PATCH 065/102] feat(append_annotation_template): add append_annotation_template function and unit tests --- .../templates/append_annotation_template.py | 139 +++++++++++ .../test_append_annotation_template.py | 223 ++++++++++++++++++ 2 files changed, 362 insertions(+) create mode 100644 src/spac/templates/append_annotation_template.py create mode 100644 tests/templates/test_append_annotation_template.py diff --git a/src/spac/templates/append_annotation_template.py b/src/spac/templates/append_annotation_template.py new file mode 100644 index 00000000..1541cf0b --- /dev/null +++ b/src/spac/templates/append_annotation_template.py @@ -0,0 +1,139 @@ +""" +Platform-agnostic Append Annotation template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.append_annotation_template import run_from_json +>>> run_from_json("examples/append_annotation_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union +import pandas as pd +import pickle + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.data_utils import append_annotation +from spac.utils import check_column_name +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], pd.DataFrame]: + """ + Execute Append Annotation analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the dataframe + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or DataFrame + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed DataFrame + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load upstream data - could be DataFrame, CSV, or pickle + upstream_dataset = params["Upstream_Dataset"] + if isinstance(upstream_dataset, pd.DataFrame): + # Direct DataFrame from previous step + input_dataframe = upstream_dataset + elif isinstance(upstream_dataset, (str, Path)): + path = Path(upstream_dataset) + if path.suffix.lower() == '.csv': + input_dataframe = pd.read_csv(path) + elif path.suffix.lower() in ['.pickle', '.pkl', '.p']: + with open(path, 'rb') as f: + input_dataframe = pickle.load(f) + else: + raise ValueError( + f"Unsupported file format: {path.suffix}. " + f"Supported formats: .csv, .pickle, .pkl" + ) + else: + raise TypeError( + f"Upstream_Dataset must be DataFrame or file path. " + f"Got {type(upstream_dataset)}" + ) + + # Extract parameters + dataset_mapping_rules = params.get( + "Annotation_Pair_List", ["Example:Example"] + ) + + # Initialize an empty dictionary + parsed_dict = {} + + # Loop through each string pair in the list + for pair in dataset_mapping_rules: + # Split the string on the colon + key, value = pair.split(":") + check_column_name(key, pair) + # Add the key-value pair to the dictionary + parsed_dict[key] = value + + print(f"The pairs to add are:\n{parsed_dict}") + + output_dataframe = append_annotation( + input_dataframe, + parsed_dict + ) + + print(output_dataframe.info()) + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "append_observations.csv") + # Ensure CSV extension for DataFrame output + if not output_file.endswith('.csv'): + output_file = output_file + '.csv' + + saved_files = save_outputs({output_file: output_dataframe}) + + print( + f"Append Annotation completed → {saved_files[output_file]}" + ) + return saved_files + else: + # Return the dataframe directly for in-memory workflows + print("Returning DataFrame (not saving to file)") + return output_dataframe + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python append_annotation_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned DataFrame") \ No newline at end of file diff --git a/tests/templates/test_append_annotation_template.py b/tests/templates/test_append_annotation_template.py new file mode 100644 index 00000000..2cdfe810 --- /dev/null +++ b/tests/templates/test_append_annotation_template.py @@ -0,0 +1,223 @@ +# tests/templates/test_append_annotation_template.py +"""Unit tests for the Append Annotation template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.append_annotation_template import run_from_json + + +def mock_dataframe(n_rows: int = 10) -> pd.DataFrame: + """Return a minimal synthetic DataFrame for fast tests.""" + data = { + "cell_id": list(range(n_rows)), + "intensity": [i * 0.5 for i in range(n_rows)], + "existing_annotation": (["TypeA", "TypeB"] * + ((n_rows + 1) // 2))[:n_rows] + } + return pd.DataFrame(data) + + +class TestAppendAnnotationTemplate(unittest.TestCase): + """Unit tests for the Append Annotation template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.csv" + ) + self.out_file = "append_observations.csv" + + # Save minimal mock data as CSV + mock_df = mock_dataframe() + mock_df.to_csv(self.in_file, index=False) + + # Minimal parameters from NIDAP template + self.params = { + "Upstream_Dataset": self.in_file, + "Annotation_Pair_List": ["region:region-A", "day:day1"], + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with CSV input and save results + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + self.assertIn(self.out_file, result) + + # Verify the output file exists + output_path = result[self.out_file] + self.assertTrue(os.path.exists(output_path)) + + # Load and verify content + output_df = pd.read_csv(output_path) + self.assertIn("region", output_df.columns) + self.assertIn("day", output_df.columns) + self.assertTrue(all(output_df["region"] == "region-A")) + self.assertTrue(all(output_df["day"] == "day1")) + + # Test 2: Run without saving + result_no_save = run_from_json(self.params, save_results=False) + # Check appropriate return type + self.assertIsInstance(result_no_save, pd.DataFrame) + self.assertIn("region", result_no_save.columns) + self.assertIn("day", result_no_save.columns) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test invalid annotation pair format (missing colon) + params_bad = self.params.copy() + params_bad["Annotation_Pair_List"] = ["invalidformat"] + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad, save_results=False) + + # The error will come from the split operation + self.assertIn("not enough values to unpack", str(context.exception)) + + @patch('spac.templates.append_annotation_template.append_annotation') + @patch('spac.templates.append_annotation_template.check_column_name') + def test_function_calls(self, mock_check, mock_append) -> None: + """Test that main functions are called with correct parameters.""" + # Mock the append_annotation function to return a dataframe + # with the expected new columns + output_df = mock_dataframe() + output_df["region"] = "region-A" + output_df["day"] = "day1" + mock_append.return_value = output_df + + result = run_from_json(self.params, save_results=False) + + # Verify check_column_name was called for each pair + self.assertEqual(mock_check.call_count, 2) + mock_check.assert_any_call("region", "region:region-A") + mock_check.assert_any_call("day", "day:day1") + + # Verify append_annotation was called correctly + mock_append.assert_called_once() + call_args = mock_append.call_args + expected_dict = {"region": "region-A", "day": "day1"} + self.assertEqual(call_args[0][1], expected_dict) + + # Verify result is correct + self.assertIsInstance(result, pd.DataFrame) + self.assertIn("region", result.columns) + self.assertIn("day", result.columns) + + def test_pickle_input(self) -> None: + """Test that pickle input files work correctly.""" + # Create pickle input file + pickle_file = os.path.join(self.tmp_dir.name, "input.pkl") + mock_df = mock_dataframe() + with open(pickle_file, 'wb') as f: + pickle.dump(mock_df, f) + + params_pickle = self.params.copy() + params_pickle["Upstream_Dataset"] = pickle_file + + result = run_from_json(params_pickle, save_results=False) + self.assertIsInstance(result, pd.DataFrame) + self.assertIn("region", result.columns) + self.assertIn("day", result.columns) + self.assertEqual(len(result), 10) + + def test_dataframe_input(self) -> None: + """Test that direct DataFrame input works correctly.""" + # Pass DataFrame directly + params_df = self.params.copy() + params_df["Upstream_Dataset"] = mock_dataframe() + + result = run_from_json(params_df, save_results=False) + self.assertIsInstance(result, pd.DataFrame) + self.assertIn("region", result.columns) + self.assertIn("day", result.columns) + self.assertEqual(len(result), 10) + + def test_unsupported_file_format(self) -> None: + """Test error for unsupported file formats.""" + # Create a file with unsupported extension + bad_file = os.path.join(self.tmp_dir.name, "input.txt") + with open(bad_file, 'w') as f: + f.write("dummy content") + + params_bad = self.params.copy() + params_bad["Upstream_Dataset"] = bad_file + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + self.assertIn("Unsupported file format: .txt", str(context.exception)) + self.assertIn("Supported formats: .csv, .pickle, .pkl", + str(context.exception)) + + def test_invalid_input_type(self) -> None: + """Test error for invalid input types.""" + params_bad = self.params.copy() + params_bad["Upstream_Dataset"] = 12345 # Invalid type + + with self.assertRaises(TypeError) as context: + run_from_json(params_bad) + + self.assertIn("Upstream_Dataset must be DataFrame or file path", + str(context.exception)) + self.assertIn("Got ", str(context.exception)) + + @patch('builtins.print') + def test_console_output(self, mock_print) -> None: + """Test that expected console output is produced.""" + run_from_json(self.params, save_results=False) + + # Collect all printed output as strings + print_output = [] + for call in mock_print.call_args_list: + if call[0]: # If there are positional arguments + print_output.append(str(call[0][0])) + + # Join all output for easier searching + all_output = '\n'.join(print_output) + + # Check for key expected outputs + # Should print the annotation pairs dictionary + self.assertIn("region", all_output) + self.assertIn("region-A", all_output) + self.assertIn("day", all_output) + self.assertIn("day1", all_output) + + # Should show DataFrame info or similar output + self.assertTrue( + "DataFrame" in all_output or + "Returning DataFrame" in all_output or + "columns" in all_output + ) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 8e500ecfe264b6125b6ace828003684e4b1b5cad Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 03:17:47 -0400 Subject: [PATCH 066/102] feat(binary_to_categorical_annotation_template): add binary_to_categorical_annotation_template function and unit tests --- ...nary_to_categorical_annotation_template.py | 130 +++++++++++++ ...nary_to_categorical_annotation_template.py | 174 ++++++++++++++++++ 2 files changed, 304 insertions(+) create mode 100644 src/spac/templates/binary_to_categorical_annotation_template.py create mode 100644 tests/templates/test_binary_to_categorical_annotation_template.py diff --git a/src/spac/templates/binary_to_categorical_annotation_template.py b/src/spac/templates/binary_to_categorical_annotation_template.py new file mode 100644 index 00000000..3cb82b69 --- /dev/null +++ b/src/spac/templates/binary_to_categorical_annotation_template.py @@ -0,0 +1,130 @@ +""" +Platform-agnostic Binary to Categorical Annotation template converted from +NIDAP. Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.binary_to_categorical_annotation_template import \ +... run_from_json +>>> run_from_json("examples/binary_to_categorical_annotation_params.json") +""" +import json +import sys +import pickle +from pathlib import Path +from typing import Any, Dict, Union +import pandas as pd + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.data_utils import bin2cat +from spac.utils import check_column_name +from spac.templates.template_utils import ( + save_outputs, + parse_params, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], pd.DataFrame]: + """ + Execute Binary to Categorical Annotation analysis with parameters from + JSON. Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the dataframe + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or DataFrame + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed DataFrame + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load upstream data - could be DataFrame, CSV, or pickle + upstream_dataset = params["Upstream_Dataset"] + if isinstance(upstream_dataset, pd.DataFrame): + input_dataset = upstream_dataset # Direct DataFrame from prev step + elif isinstance(upstream_dataset, (str, Path)): + path = Path(upstream_dataset) + if path.suffix.lower() == '.csv': + input_dataset = pd.read_csv(path) + elif path.suffix.lower() in ['.pickle', '.pkl', '.p']: + with open(path, 'rb') as f: + input_dataset = pickle.load(f) + else: + raise ValueError( + f"Unsupported file format: {path.suffix}. " + f"Supported formats: .csv, .pickle, .pkl, .p" + ) + else: + raise TypeError( + f"Upstream_Dataset must be DataFrame or file path. " + f"Got {type(upstream_dataset)}" + ) + + # Extract parameters + one_hot_annotations = params.get( + "Binary_Annotation_Columns", + ["Normal_Cells", "Cancer_Cells", "Immuno_Cells"] + ) + new_annotation = params.get("New_Annotation_Name", "cell_labels") + + check_column_name(new_annotation, "New Annotation Name") + + converted_df = bin2cat( + data=input_dataset, + one_hot_annotations=one_hot_annotations, + new_annotation=new_annotation + ) + + print(converted_df.info()) + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get( + "Output_File", "converted_annotations.csv" + ) + + saved_files = save_outputs({output_file: converted_df}) + + print( + f"Binary to Categorical Annotation completed → " + f"{saved_files[output_file]}" + ) + return saved_files + else: + # Return the dataframe directly for in-memory workflows + print("Returning DataFrame (not saving to file)") + return converted_df + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python binary_to_categorical_annotation_template.py " + "", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned DataFrame") \ No newline at end of file diff --git a/tests/templates/test_binary_to_categorical_annotation_template.py b/tests/templates/test_binary_to_categorical_annotation_template.py new file mode 100644 index 00000000..eb5cb2f5 --- /dev/null +++ b/tests/templates/test_binary_to_categorical_annotation_template.py @@ -0,0 +1,174 @@ +# tests/templates/test_binary_to_categorical_annotation_template.py +"""Unit tests for the Binary to Categorical Annotation template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.binary_to_categorical_annotation_template import ( + run_from_json +) + + +def mock_dataframe(n_cells: int = 10) -> pd.DataFrame: + """Return a minimal synthetic DataFrame for fast tests.""" + # Create binary annotation columns with mutually exclusive values + data = { + "cell_id": [f"cell_{i}" for i in range(n_cells)], + "Normal_Cells": [0] * n_cells, + "Cancer_Cells": [0] * n_cells, + "Immuno_Cells": [0] * n_cells, + "x_centroid": [100.0 + i for i in range(n_cells)], + "y_centroid": [200.0 + i for i in range(n_cells)] + } + + # Make mutually exclusive binary annotations + for i in range(n_cells): + if i % 3 == 0: + data["Normal_Cells"][i] = 1 + elif i % 3 == 1: + data["Cancer_Cells"][i] = 1 + else: + data["Immuno_Cells"][i] = 1 + + return pd.DataFrame(data) + + +class TestBinaryToCategoricalAnnotationTemplate(unittest.TestCase): + """Unit tests for the Binary to Categorical Annotation template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.csv" + ) + self.out_file = "converted_annotations.csv" + + # Save minimal mock data + mock_df = mock_dataframe() + mock_df.to_csv(self.in_file, index=False) + + # Minimal parameters from NIDAP template + self.params = { + "Upstream_Dataset": self.in_file, + "Binary_Annotation_Columns": [ + "Normal_Cells", + "Cancer_Cells", + "Immuno_Cells" + ], + "New_Annotation_Name": "cell_labels", + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters (CSV input) + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + self.assertIn(self.out_file, result) + + # Verify the output file exists and has expected content + output_path = result[self.out_file] + self.assertTrue(os.path.exists(output_path)) + + # Load and check the converted dataframe + converted_df = pd.read_csv(output_path) + self.assertIn("cell_labels", converted_df.columns) + # Check that categorical values were created + unique_labels = set(converted_df["cell_labels"]) + expected_labels = {"Normal_Cells", "Cancer_Cells", "Immuno_Cells"} + self.assertEqual(unique_labels, expected_labels) + + # Test 2: Run without saving + result_no_save = run_from_json(self.params, save_results=False) + self.assertIsInstance(result_no_save, pd.DataFrame) + self.assertIn("cell_labels", result_no_save.columns) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + # Test 4: Pickle file input + pickle_file = os.path.join(self.tmp_dir.name, "input.pkl") + mock_df = mock_dataframe() + with open(pickle_file, 'wb') as f: + pickle.dump(mock_df, f) + + params_pickle = self.params.copy() + params_pickle["Upstream_Dataset"] = pickle_file + result_pickle = run_from_json(params_pickle, save_results=False) + self.assertIsInstance(result_pickle, pd.DataFrame) + self.assertIn("cell_labels", result_pickle.columns) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test 1: Invalid column name with special characters + params_bad = self.params.copy() + params_bad["New_Annotation_Name"] = "cell@labels!" + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + # Check that check_column_name was triggered + self.assertIn("New Annotation Name", str(context.exception)) + + # Test 2: Unsupported file format + params_bad_format = self.params.copy() + params_bad_format["Upstream_Dataset"] = "input.txt" + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad_format) + + expected_msg = ( + "Unsupported file format: .txt. " + "Supported formats: .csv, .pickle, .pkl, .p" + ) + self.assertEqual(str(context.exception), expected_msg) + + @patch('spac.templates.binary_to_categorical_annotation_template.bin2cat') + def test_function_calls(self, mock_bin2cat) -> None: + """Test that main function is called with correct parameters.""" + # Mock the bin2cat function to return a dataframe + mock_result_df = mock_dataframe() + mock_result_df["cell_labels"] = ["Normal_Cells"] * len(mock_result_df) + mock_bin2cat.return_value = mock_result_df + + run_from_json(self.params, save_results=False) + + # Verify function was called correctly + mock_bin2cat.assert_called_once() + call_args = mock_bin2cat.call_args + + # Check the arguments + self.assertIsInstance(call_args[1]['data'], pd.DataFrame) + self.assertEqual( + call_args[1]['one_hot_annotations'], + ["Normal_Cells", "Cancer_Cells", "Immuno_Cells"] + ) + self.assertEqual(call_args[1]['new_annotation'], "cell_labels") + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 4fea9c336dabc207d6fa66de8553d3fe89c9dd62 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 03:45:10 -0400 Subject: [PATCH 067/102] feat(calculate_centroid_template): add calculate_centroid_template function and unit tests --- .../templates/calculate_centroid_template.py | 132 ++++++++++++++ .../test_calculate_centroid_template.py | 166 ++++++++++++++++++ 2 files changed, 298 insertions(+) create mode 100644 src/spac/templates/calculate_centroid_template.py create mode 100644 tests/templates/test_calculate_centroid_template.py diff --git a/src/spac/templates/calculate_centroid_template.py b/src/spac/templates/calculate_centroid_template.py new file mode 100644 index 00000000..e6f56658 --- /dev/null +++ b/src/spac/templates/calculate_centroid_template.py @@ -0,0 +1,132 @@ +""" +Platform-agnostic Calculate Centroid template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.calculate_centroid_template import run_from_json +>>> run_from_json("examples/calculate_centroid_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union +import pandas as pd +import pickle + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.data_utils import calculate_centroid +from spac.utils import check_column_name +from spac.templates.template_utils import ( + save_outputs, + parse_params, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], pd.DataFrame]: + """ + Execute Calculate Centroid analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the DataFrame + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or DataFrame + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed DataFrame + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load upstream data - could be DataFrame, CSV, or pickle + upstream_dataset = params["Upstream_Dataset"] + if isinstance(upstream_dataset, pd.DataFrame): + input_dataset = upstream_dataset # Direct DataFrame from previous step + elif isinstance(upstream_dataset, (str, Path)): + path = Path(upstream_dataset) + if path.suffix.lower() == '.csv': + input_dataset = pd.read_csv(path) + elif path.suffix.lower() in ['.pickle', '.pkl', '.p']: + with open(path, 'rb') as f: + input_dataset = pickle.load(f) + else: + raise ValueError( + f"Unsupported file format: {path.suffix}. " + f"Supported formats: .csv, .pickle, .pkl, .p" + ) + else: + raise TypeError( + f"Upstream_Dataset must be DataFrame or file path. " + f"Got {type(upstream_dataset)}" + ) + + # Extract parameters using .get() with defaults from JSON template + x_min = params.get("Min_X_Coordinate_Column_Name", "XMin") + x_max = params.get("Max_X_Coordinate_Column_Name", "XMax") + y_min = params.get("Min_Y_Coordinate_Column_Name", "YMin") + y_max = params.get("Max_Y_Coordinate_Column_Name", "YMax") + new_x = params.get("X_Centroid_Name", "XCentroid") + new_y = params.get("Y_Centroid_Name", "YCentroid") + + check_column_name(new_x, "X Centroid Name") + check_column_name(new_y, "Y Centroid Name") + + centroid_calculated = calculate_centroid( + input_dataset, + x_min=x_min, + x_max=x_max, + y_min=y_min, + y_max=y_max, + new_x=new_x, + new_y=new_y + ) + + print(centroid_calculated.info()) + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "centroid_calculated.csv") + # Default to CSV format if no recognized extension + if not output_file.endswith(('.csv', '.pickle', '.pkl')): + output_file = output_file + '.csv' + + saved_files = save_outputs({output_file: centroid_calculated}) + + print(f"Calculate Centroid completed → {saved_files[output_file]}") + return saved_files + else: + # Return the DataFrame directly for in-memory workflows + print("Returning DataFrame (not saving to file)") + return centroid_calculated + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python calculate_centroid_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned DataFrame") \ No newline at end of file diff --git a/tests/templates/test_calculate_centroid_template.py b/tests/templates/test_calculate_centroid_template.py new file mode 100644 index 00000000..d3c98595 --- /dev/null +++ b/tests/templates/test_calculate_centroid_template.py @@ -0,0 +1,166 @@ +# tests/templates/test_calculate_centroid_template.py +"""Unit tests for the Calculate Centroid template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import pandas as pd +import numpy as np +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.calculate_centroid_template import run_from_json + + +def mock_dataframe(n_cells: int = 10) -> pd.DataFrame: + """Return a minimal synthetic DataFrame for fast tests.""" + rng = np.random.default_rng(0) + return pd.DataFrame({ + "XMin": rng.uniform(0, 50, n_cells), + "XMax": rng.uniform(50, 100, n_cells), + "YMin": rng.uniform(0, 50, n_cells), + "YMax": rng.uniform(50, 100, n_cells), + "CellID": range(n_cells), + "CellType": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells] + }) + + +class TestCalculateCentroidTemplate(unittest.TestCase): + """Unit tests for the Calculate Centroid template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.csv" + ) + self.out_file = "centroid_calculated" + + # Save minimal mock data as CSV + mock_df = mock_dataframe() + mock_df.to_csv(self.in_file, index=False) + + # Minimal parameters from JSON template + self.params = { + "Upstream_Dataset": self.in_file, + "Min_X_Coordinate_Column_Name": "XMin", + "Max_X_Coordinate_Column_Name": "XMax", + "Min_Y_Coordinate_Column_Name": "YMin", + "Max_Y_Coordinate_Column_Name": "YMax", + "X_Centroid_Name": "XCentroid", + "Y_Centroid_Name": "YCentroid", + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters (CSV input) + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + # Verify file was saved + self.assertTrue(len(result) > 0) + csv_files = [f for f in result.values() if '.csv' in str(f)] + self.assertTrue(len(csv_files) > 0) + + # Test 2: Run without saving + result_no_save = run_from_json(self.params, save_results=False) + # Check appropriate return type + self.assertIsInstance(result_no_save, pd.DataFrame) + # Verify centroids were calculated + self.assertIn("XCentroid", result_no_save.columns) + self.assertIn("YCentroid", result_no_save.columns) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + # Test 4: Pickle file input + pickle_file = os.path.join(self.tmp_dir.name, "input.pickle") + mock_df = mock_dataframe() + with open(pickle_file, 'wb') as f: + pickle.dump(mock_df, f) + + params_pickle = self.params.copy() + params_pickle["Upstream_Dataset"] = pickle_file + result_pickle = run_from_json(params_pickle, save_results=False) + self.assertIsInstance(result_pickle, pd.DataFrame) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test unsupported file format + params_bad = self.params.copy() + params_bad["Upstream_Dataset"] = "invalid.txt" + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + # Check error message + self.assertIn("Unsupported file format", str(context.exception)) + + @patch('spac.templates.calculate_centroid_template.calculate_centroid') + def test_function_calls(self, mock_calc) -> None: + """Test that main function is called with correct parameters.""" + # Mock the calculate_centroid function + mock_df = mock_dataframe() + mock_df["XCentroid"] = (mock_df["XMin"] + mock_df["XMax"]) / 2 + mock_df["YCentroid"] = (mock_df["YMin"] + mock_df["YMax"]) / 2 + mock_calc.return_value = mock_df + + run_from_json(self.params, save_results=False) + + # Verify function was called correctly + mock_calc.assert_called_once() + call_kwargs = mock_calc.call_args[1] + + # Check specific parameter conversions + self.assertEqual(call_kwargs['x_min'], "XMin") + self.assertEqual(call_kwargs['x_max'], "XMax") + self.assertEqual(call_kwargs['y_min'], "YMin") + self.assertEqual(call_kwargs['y_max'], "YMax") + self.assertEqual(call_kwargs['new_x'], "XCentroid") + self.assertEqual(call_kwargs['new_y'], "YCentroid") + + def test_direct_dataframe_input(self) -> None: + """Test that DataFrame can be passed directly.""" + mock_df = mock_dataframe() + params_df = self.params.copy() + params_df["Upstream_Dataset"] = mock_df + + calc_centroid_patch = ( + 'spac.templates.calculate_centroid_template.calculate_centroid' + ) + with patch(calc_centroid_patch) as mock_calc: + # Mock return value + result_df = mock_df.copy() + result_df["XCentroid"] = 75.0 + result_df["YCentroid"] = 75.0 + mock_calc.return_value = result_df + + result = run_from_json(params_df, save_results=False) + self.assertIsInstance(result, pd.DataFrame) + + # Verify the input DataFrame was passed correctly + call_args = mock_calc.call_args[0] + pd.testing.assert_frame_equal(call_args[0], mock_df) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From e59c994352f09ee1398f9cea6cc02041daa0cd03 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 04:10:18 -0400 Subject: [PATCH 068/102] feat(select_values_template): add select_values_template function and unit tests --- src/spac/templates/select_values_template.py | 122 ++++++++++++++ .../templates/test_select_values_template.py | 150 ++++++++++++++++++ 2 files changed, 272 insertions(+) create mode 100644 src/spac/templates/select_values_template.py create mode 100644 tests/templates/test_select_values_template.py diff --git a/src/spac/templates/select_values_template.py b/src/spac/templates/select_values_template.py new file mode 100644 index 00000000..e454452d --- /dev/null +++ b/src/spac/templates/select_values_template.py @@ -0,0 +1,122 @@ +""" +Platform-agnostic Select Values template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.select_values_template import run_from_json +>>> run_from_json("examples/select_values_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union +import pandas as pd +import warnings +import pickle + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.data_utils import select_values +from spac.templates.template_utils import ( + save_outputs, + parse_params, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], pd.DataFrame]: + """ + Execute Select Values analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the dataframe + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or DataFrame + If save_results=True: Dictionary of saved file paths + If save_results=False: The filtered DataFrame + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load upstream data - could be DataFrame, CSV, or pickle + upstream_dataset = params["Upstream_Dataset"] + + if isinstance(upstream_dataset, pd.DataFrame): + input_dataset = upstream_dataset # Direct DataFrame from previous step + elif isinstance(upstream_dataset, (str, Path)): + path = Path(upstream_dataset) + if path.suffix.lower() == '.csv': + input_dataset = pd.read_csv(path) + elif path.suffix.lower() in ['.pickle', '.pkl', '.p']: + with open(path, 'rb') as f: + input_dataset = pickle.load(f) + else: + raise ValueError( + f"Unsupported file format: {path.suffix}. " + f"Supported formats: .csv, .pickle, .pkl, .p" + ) + else: + raise TypeError( + f"Upstream_Dataset must be DataFrame or file path. " + f"Got {type(upstream_dataset)}" + ) + + # Extract parameters + observation = params["Annotation_of_Interest"] + values = params["Label_s_of_Interest"] + + with warnings.catch_warnings(record=True) as caught_warnings: + filtered_dataset = select_values( + data=input_dataset, + annotation=observation, + values=values + ) + if caught_warnings is not None: + for warning in caught_warnings: + raise ValueError(warning.message) + + print(filtered_dataset.info()) + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "select_values.csv") + saved_files = save_outputs({output_file: filtered_dataset}) + + print(f"Select Values completed → {saved_files[output_file]}") + return saved_files + else: + # Return the dataframe directly for in-memory workflows + print("Returning DataFrame (not saving to file)") + return filtered_dataset + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python select_values_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned DataFrame") \ No newline at end of file diff --git a/tests/templates/test_select_values_template.py b/tests/templates/test_select_values_template.py new file mode 100644 index 00000000..edf343c0 --- /dev/null +++ b/tests/templates/test_select_values_template.py @@ -0,0 +1,150 @@ +# tests/templates/test_select_values_template.py +"""Unit tests for the Select Values template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.select_values_template import run_from_json + + +def mock_dataframe(n_rows: int = 10) -> pd.DataFrame: + """Return a minimal synthetic DataFrame for fast tests.""" + return pd.DataFrame({ + "file_name": [f"Halo_Synthetic_Example_{i % 3 + 1}" + for i in range(n_rows)], + "cell_type": (["TypeA", "TypeB"] * ((n_rows + 1) // 2))[:n_rows], + "marker_value": range(n_rows) + }) + + +class TestSelectValuesTemplate(unittest.TestCase): + """Unit tests for the Select Values template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.csv" + ) + self.out_file = "select_values.csv" + + # Save minimal mock data + mock_df = mock_dataframe() + mock_df.to_csv(self.in_file, index=False) + + # Minimal parameters from NIDAP template + self.params = { + "Upstream_Dataset": self.in_file, + "Annotation_of_Interest": "file_name", + "Label_s_of_Interest": ["Halo_Synthetic_Example_1"], + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + self.assertIn(self.out_file, result) + + # Verify the output file exists and has correct content + output_path = result[self.out_file] + self.assertTrue(os.path.exists(output_path)) + + # Read and verify filtered data + filtered_df = pd.read_csv(output_path) + self.assertEqual( + filtered_df["file_name"].unique().tolist(), + ["Halo_Synthetic_Example_1"] + ) + + # Test 2: Run without saving + result_no_save = run_from_json(self.params, save_results=False) + self.assertIsInstance(result_no_save, pd.DataFrame) + self.assertEqual( + result_no_save["file_name"].unique().tolist(), + ["Halo_Synthetic_Example_1"] + ) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test with non-existent annotation + params_bad = self.params.copy() + params_bad["Annotation_of_Interest"] = "non_existent_column" + + with self.assertRaises(Exception): + # The select_values function should raise an error + run_from_json(params_bad) + + @patch('spac.templates.select_values_template.select_values') + def test_function_calls(self, mock_select) -> None: + """Test that main function is called with correct parameters.""" + # Mock the select_values function + mock_df = mock_dataframe(3) + mock_select.return_value = mock_df + + run_from_json(self.params) + + # Verify function was called correctly + mock_select.assert_called_once() + call_args = mock_select.call_args + + # Check the arguments + self.assertEqual(call_args[1]['annotation'], "file_name") + self.assertEqual( + call_args[1]['values'], + ["Halo_Synthetic_Example_1"] + ) + + def test_multiple_file_formats(self) -> None: + """Test loading from different file formats.""" + # Test CSV (already covered above) + + # Test pickle format + pickle_file = os.path.join(self.tmp_dir.name, "input.pkl") + mock_df = mock_dataframe() + with open(pickle_file, 'wb') as f: + pickle.dump(mock_df, f) + + params_pickle = self.params.copy() + params_pickle["Upstream_Dataset"] = pickle_file + + result = run_from_json(params_pickle, save_results=False) + self.assertIsInstance(result, pd.DataFrame) + + # Test direct DataFrame input + params_df = self.params.copy() + params_df["Upstream_Dataset"] = mock_df + + result = run_from_json(params_df, save_results=False) + self.assertIsInstance(result, pd.DataFrame) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From cedb6d163d1ffc7383961c0291a20a474f35843f Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 08:24:46 -0400 Subject: [PATCH 069/102] fix(select_values_template): fix pandas/numpy version compatibility issue --- src/spac/templates/select_values_template.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/spac/templates/select_values_template.py b/src/spac/templates/select_values_template.py index e454452d..223105cd 100644 --- a/src/spac/templates/select_values_template.py +++ b/src/spac/templates/select_values_template.py @@ -76,16 +76,24 @@ def run_from_json( # Extract parameters observation = params["Annotation_of_Interest"] values = params["Label_s_of_Interest"] - + with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter("always") filtered_dataset = select_values( data=input_dataset, annotation=observation, values=values ) - if caught_warnings is not None: + # Only process warnings that are relevant to the select_values operation + if caught_warnings: for warning in caught_warnings: - raise ValueError(warning.message) + # Skip deprecation warnings from numpy/pandas + if (hasattr(warning, 'category') and + issubclass(warning.category, DeprecationWarning)): + continue + # Raise actual operational warnings as errors + if hasattr(warning, 'message'): + raise ValueError(str(warning.message)) print(filtered_dataset.info()) From 47adf3e881f28d05b4627b1ec3d0954b403f634e Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 09:06:18 -0400 Subject: [PATCH 070/102] feat(downsample_cells_template): add downsample_cells_template function and unit tests --- .../templates/downsample_cells_template.py | 139 +++++++++++++++ .../test_downsample_cells_template.py | 161 ++++++++++++++++++ 2 files changed, 300 insertions(+) create mode 100644 src/spac/templates/downsample_cells_template.py create mode 100644 tests/templates/test_downsample_cells_template.py diff --git a/src/spac/templates/downsample_cells_template.py b/src/spac/templates/downsample_cells_template.py new file mode 100644 index 00000000..90d36136 --- /dev/null +++ b/src/spac/templates/downsample_cells_template.py @@ -0,0 +1,139 @@ +""" +Platform-agnostic Downsample Cells template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.downsample_cells_template import run_from_json +>>> run_from_json("examples/downsample_cells_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union +import pandas as pd +import pickle + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.data_utils import downsample_cells +from spac.utils import check_column_name +from spac.templates.template_utils import ( + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], pd.DataFrame]: + """ + Execute Downsample Cells analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the dataframe + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or DataFrame + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed DataFrame + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load upstream data - could be DataFrame, CSV, or pickle + upstream_dataset = params["Upstream_Dataset"] + if isinstance(upstream_dataset, pd.DataFrame): + input_dataset = upstream_dataset # Direct DF from previous step + elif isinstance(upstream_dataset, (str, Path)): + path = Path(upstream_dataset) + if path.suffix.lower() == '.csv': + input_dataset = pd.read_csv(path) + elif path.suffix.lower() in ['.pickle', '.pkl', '.p']: + with open(path, 'rb') as f: + input_dataset = pickle.load(f) + else: + raise ValueError( + f"Unsupported file format: {path.suffix}. " + f"Supported formats: .csv, .pickle, .pkl, .p" + ) + else: + raise TypeError( + f"Upstream_Dataset must be DataFrame or file path. " + f"Got {type(upstream_dataset)}" + ) + + # Extract parameters + annotations = params["Annotations_List"] + n_samples = params["Number_of_Samples"] + stratify = params["Stratify_Option"] + rand = params["Random_Selection"] + combined_col_name = params.get( + "New_Combined_Annotation_Name", "_combined_" + ) + min_threshold = params.get("Minimum_Threshold", 5) + + check_column_name( + combined_col_name, "New Combined Annotation Name" + ) + + down_sampled_dataset = downsample_cells( + input_data=input_dataset, + annotations=annotations, + n_samples=n_samples, + stratify=stratify, + rand=rand, + combined_col_name=combined_col_name, + min_threshold=min_threshold + ) + + print("Downsampled! Processed dataset info:") + print(down_sampled_dataset.info()) + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "downsampled_data.csv") + # Default to CSV format if no recognized extension + if not output_file.endswith(('.csv', '.pickle', '.pkl')): + output_file = output_file + '.csv' + + saved_files = save_outputs({output_file: down_sampled_dataset}) + + print( + f"Downsample Cells completed → {saved_files[output_file]}" + ) + return saved_files + else: + # Return the dataframe directly for in-memory workflows + print("Returning DataFrame (not saving to file)") + return down_sampled_dataset + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python downsample_cells_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned DataFrame") \ No newline at end of file diff --git a/tests/templates/test_downsample_cells_template.py b/tests/templates/test_downsample_cells_template.py new file mode 100644 index 00000000..f607b417 --- /dev/null +++ b/tests/templates/test_downsample_cells_template.py @@ -0,0 +1,161 @@ +# tests/templates/test_downsample_cells_template.py +"""Unit tests for the Downsample Cells template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import pandas as pd +import numpy as np +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.downsample_cells_template import run_from_json + + +def mock_dataframe(n_rows: int = 1000) -> pd.DataFrame: + """Return a minimal synthetic DataFrame for fast tests.""" + rng = np.random.default_rng(0) + + # Create a dataframe with multiple annotations for downsampling + data = { + "cell_id": range(n_rows), + "region": rng.choice(["region1", "region2", "region3"], n_rows), + "day": rng.choice(["day1", "day2", "day3", "day4"], n_rows), + "cell_type": rng.choice(["TypeA", "TypeB", "TypeC"], n_rows), + "marker1": rng.normal(100, 15, n_rows), + "marker2": rng.normal(50, 10, n_rows), + } + + return pd.DataFrame(data) + + +class TestDownsampleCellsTemplate(unittest.TestCase): + """Unit tests for the Downsample Cells template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.csv" + ) + self.out_file = "downsampled_data" + + # Save minimal mock data as CSV + mock_df = mock_dataframe() + mock_df.to_csv(self.in_file, index=False) + + # Minimal parameters from NIDAP template + self.params = { + "Upstream_Dataset": self.in_file, + "Annotations_List": ["region", "day"], + "Number_of_Samples": 100, + "Stratify_Option": False, + "Random_Selection": True, + "New_Combined_Annotation_Name": "_combined_", + "Minimum_Threshold": 5, + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + + # Verify file was saved with correct extension + self.assertTrue(len(result) > 0) + saved_file = list(result.values())[0] + self.assertTrue(saved_file.endswith('.csv')) + + # Test 2: Run without saving + result_no_save = run_from_json(self.params, save_results=False) + # Check appropriate return type + self.assertIsInstance(result_no_save, pd.DataFrame) + # Verify downsampling occurred + self.assertLessEqual(len(result_no_save), 1000) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + def test_different_input_formats(self) -> None: + """Test loading from different file formats.""" + # Test pickle input + pickle_file = os.path.join(self.tmp_dir.name, "input.pickle") + mock_df = mock_dataframe(500) + with open(pickle_file, 'wb') as f: + pickle.dump(mock_df, f) + + params_pickle = self.params.copy() + params_pickle["Upstream_Dataset"] = pickle_file + + result = run_from_json(params_pickle, save_results=False) + self.assertIsInstance(result, pd.DataFrame) + + # Test direct DataFrame input + params_df = self.params.copy() + params_df["Upstream_Dataset"] = mock_dataframe(300) + + result = run_from_json(params_df, save_results=False) + self.assertIsInstance(result, pd.DataFrame) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test unsupported file format + params_bad = self.params.copy() + params_bad["Upstream_Dataset"] = "data.xlsx" + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + # Check error message contains expected text + self.assertIn("Unsupported file format", str(context.exception)) + self.assertIn(".xlsx", str(context.exception)) + + @patch('spac.templates.downsample_cells_template.downsample_cells') + def test_function_calls(self, mock_downsample) -> None: + """Test that main function is called with correct parameters.""" + # Mock the downsample function + mock_downsample.return_value = pd.DataFrame({"col": [1, 2, 3]}) + + # Test with stratify option + params_stratify = self.params.copy() + params_stratify["Stratify_Option"] = True + params_stratify["Random_Selection"] = False + params_stratify["Number_of_Samples"] = 50 + + run_from_json(params_stratify, save_results=False) + + # Verify function was called correctly + mock_downsample.assert_called_once() + call_args = mock_downsample.call_args + + # Check specific parameter conversions + self.assertEqual(call_args[1]['annotations'], ["region", "day"]) + self.assertEqual(call_args[1]['n_samples'], 50) + self.assertEqual(call_args[1]['stratify'], True) + self.assertEqual(call_args[1]['rand'], False) + self.assertEqual(call_args[1]['combined_col_name'], "_combined_") + self.assertEqual(call_args[1]['min_threshold'], 5) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 3e24237c5a4e3f57e09c0396532200dcdf471990 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 09:37:16 -0400 Subject: [PATCH 071/102] feat(combine_dataframes_template): add combine_dataframes_template function and unit tests --- .../templates/combine_dataframes_template.py | 139 +++++++++++++++ .../test_combine_dataframes_template.py | 160 ++++++++++++++++++ 2 files changed, 299 insertions(+) create mode 100644 src/spac/templates/combine_dataframes_template.py create mode 100644 tests/templates/test_combine_dataframes_template.py diff --git a/src/spac/templates/combine_dataframes_template.py b/src/spac/templates/combine_dataframes_template.py new file mode 100644 index 00000000..142d6517 --- /dev/null +++ b/src/spac/templates/combine_dataframes_template.py @@ -0,0 +1,139 @@ +""" +Platform-agnostic Combine DataFrames template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.combine_dataframes_template import run_from_json +>>> run_from_json("examples/combine_dataframes_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union +import pandas as pd +import pickle + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +# Import SPAC functions from NIDAP template +from spac.data_utils import combine_dfs +from spac.templates.template_utils import ( + save_outputs, + parse_params, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], pd.DataFrame]: + """ + Execute Combine DataFrames analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the dataframe + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or DataFrame + If save_results=True: Dictionary of saved file paths + If save_results=False: The combined DataFrame + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the first dataframe + dataset_A = params["First_Dataframe"] + if isinstance(dataset_A, pd.DataFrame): + dataset_A = dataset_A # Direct DataFrame from previous step + elif isinstance(dataset_A, (str, Path)): + path = Path(dataset_A) + if path.suffix.lower() == '.csv': + dataset_A = pd.read_csv(path) + elif path.suffix.lower() in ['.pickle', '.pkl', '.p']: + with open(path, 'rb') as f: + dataset_A = pickle.load(f) + else: + raise ValueError( + f"Unsupported file format: {path.suffix}. " + f"Supported formats: .csv, .pickle, .pkl, .p" + ) + else: + raise TypeError( + f"First_Dataframe must be DataFrame or file path. " + f"Got {type(dataset_A)}" + ) + + # Load the second dataframe + dataset_B = params["Second_Dataframe"] + if isinstance(dataset_B, pd.DataFrame): + dataset_B = dataset_B # Direct DataFrame from previous step + elif isinstance(dataset_B, (str, Path)): + path = Path(dataset_B) + if path.suffix.lower() == '.csv': + dataset_B = pd.read_csv(path) + elif path.suffix.lower() in ['.pickle', '.pkl', '.p']: + with open(path, 'rb') as f: + dataset_B = pickle.load(f) + else: + raise ValueError( + f"Unsupported file format: {path.suffix}. " + f"Supported formats: .csv, .pickle, .pkl, .p" + ) + else: + raise TypeError( + f"Second_Dataframe must be DataFrame or file path. " + f"Got {type(dataset_B)}" + ) + + # Extract parameters + input_df_lists = [dataset_A, dataset_B] + + print("Information about the first dataset:") + print(dataset_A.info()) + print("\n\nInformation about the second dataset:") + print(dataset_B.info()) + + combined_dfs = combine_dfs(input_df_lists) + print("\n\nInformation about the combined dataset:") + print(combined_dfs.info()) + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "combined_dataframes.csv") + saved_files = save_outputs({output_file: combined_dfs}) + + print(f"Combine DataFrames completed → {saved_files[output_file]}") + return saved_files + else: + # Return the dataframe directly for in-memory workflows + print("Returning combined DataFrame (not saving to file)") + return combined_dfs + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python combine_dataframes_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned combined DataFrame") \ No newline at end of file diff --git a/tests/templates/test_combine_dataframes_template.py b/tests/templates/test_combine_dataframes_template.py new file mode 100644 index 00000000..7a62a38b --- /dev/null +++ b/tests/templates/test_combine_dataframes_template.py @@ -0,0 +1,160 @@ +# tests/templates/test_combine_dataframes_template.py +"""Unit tests for the Combine DataFrames template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.combine_dataframes_template import run_from_json + + +def mock_dataframe(n_rows: int = 10, prefix: str = "") -> pd.DataFrame: + """Return a minimal synthetic DataFrame for fast tests.""" + return pd.DataFrame({ + f'{prefix}col1': range(n_rows), + f'{prefix}col2': [f'val_{i}' for i in range(n_rows)], + f'{prefix}col3': [i * 2.5 for i in range(n_rows)] + }) + + +class TestCombineDataFramesTemplate(unittest.TestCase): + """Unit tests for the Combine DataFrames template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + + # Create test CSV files + self.csv_file1 = os.path.join( + self.tmp_dir.name, "dataframe1.csv" + ) + self.csv_file2 = os.path.join( + self.tmp_dir.name, "dataframe2.csv" + ) + + # Create test pickle files + self.pkl_file1 = os.path.join( + self.tmp_dir.name, "dataframe1.pkl" + ) + self.pkl_file2 = os.path.join( + self.tmp_dir.name, "dataframe2.pkl" + ) + + # Save test dataframes + df1 = mock_dataframe(10, 'A_') + df2 = mock_dataframe(15, 'B_') + + df1.to_csv(self.csv_file1, index=False) + df2.to_csv(self.csv_file2, index=False) + + with open(self.pkl_file1, 'wb') as f: + pickle.dump(df1, f) + with open(self.pkl_file2, 'wb') as f: + pickle.dump(df2, f) + + self.out_file = "combined_output.csv" + + # Minimal parameters + self.params = { + "First_Dataframe": self.csv_file1, + "Second_Dataframe": self.csv_file2, + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with CSV files + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + self.assertIn(self.out_file, result) + + # Verify combined file exists and has correct shape + combined_df = pd.read_csv(result[self.out_file]) + self.assertEqual(len(combined_df), 25) # 10 + 15 rows + self.assertEqual(len(combined_df.columns), 6) # 3 + 3 columns + + # Test 2: Run with pickle files + params_pkl = self.params.copy() + params_pkl["First_Dataframe"] = self.pkl_file1 + params_pkl["Second_Dataframe"] = self.pkl_file2 + + result_pkl = run_from_json(params_pkl) + self.assertIsInstance(result_pkl, dict) + + # Test 3: Run without saving + result_no_save = run_from_json(self.params, save_results=False) + self.assertIsInstance(result_no_save, pd.DataFrame) + self.assertEqual(len(result_no_save), 25) + + # Test 4: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test unsupported file format + params_bad = self.params.copy() + params_bad["First_Dataframe"] = "file.txt" + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + # Check error message contains expected text + self.assertIn("Unsupported file format", str(context.exception)) + self.assertIn(".txt", str(context.exception)) + + @patch('spac.templates.combine_dataframes_template.combine_dfs') + def test_function_calls(self, mock_combine) -> None: + """Test that main function is called with correct parameters.""" + # Mock the combine_dfs function + mock_combine.return_value = mock_dataframe(25, 'combined_') + + run_from_json(self.params) + + # Verify function was called correctly + mock_combine.assert_called_once() + # Check that two dataframes were passed + call_args = mock_combine.call_args[0][0] + self.assertEqual(len(call_args), 2) + self.assertIsInstance(call_args[0], pd.DataFrame) + self.assertIsInstance(call_args[1], pd.DataFrame) + + def test_direct_dataframe_input(self) -> None: + """Test passing DataFrames directly instead of file paths.""" + df1 = mock_dataframe(5, 'X_') + df2 = mock_dataframe(8, 'Y_') + + params_df = { + "First_Dataframe": df1, + "Second_Dataframe": df2, + "Output_File": "direct_df_output.csv" + } + + result = run_from_json(params_df, save_results=False) + self.assertIsInstance(result, pd.DataFrame) + self.assertEqual(len(result), 13) # 5 + 8 rows + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 1db00a88dfdfbbc68862b3aac99aea5332e2255c Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:09:42 -0400 Subject: [PATCH 072/102] feat(subset_analysis_template): add subset_analysis_template function and unit tests --- .../templates/subset_analysis_template.py | 133 ++++++++++++++++ .../test_subset_analysis_template.py | 148 ++++++++++++++++++ 2 files changed, 281 insertions(+) create mode 100644 src/spac/templates/subset_analysis_template.py create mode 100644 tests/templates/test_subset_analysis_template.py diff --git a/src/spac/templates/subset_analysis_template.py b/src/spac/templates/subset_analysis_template.py new file mode 100644 index 00000000..b6242e20 --- /dev/null +++ b/src/spac/templates/subset_analysis_template.py @@ -0,0 +1,133 @@ +""" +Platform-agnostic Subset Analysis template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.subset_analysis_template import run_from_json +>>> run_from_json("examples/subset_analysis_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, List +import pandas as pd +import warnings + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +# Import SPAC functions from NIDAP template +from spac.data_utils import select_values +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], Any]: + """ + Execute Subset Analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the adata object + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or AnnData + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed AnnData object + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + # Use direct dictionary access for required parameters (NIDAP style) + annotation = params["Annotation_of_interest"] + labels = params["Labels"] + + # Use .get() with defaults for optional parameters from JSON template + toogle = params.get("Include_Exclude", "Include Selected Labels") + + if toogle == "Include Selected Labels": + values_to_include = labels + values_to_exclude = None + else: + values_to_include = None + values_to_exclude = labels + + with warnings.catch_warnings(record=True) as caught_warnings: + filtered_adata = select_values( + data=adata, + annotation=annotation, + values=values_to_include, + exclude_values=values_to_exclude + ) + if caught_warnings is not None: + for warning in caught_warnings: + raise ValueError(warning.message) + + print(filtered_adata) + print("\n") + + # Count and display occurrences of each label in the annotation + label_counts = filtered_adata.obs[annotation].value_counts() + print(label_counts) + print("\n") + + dataframe = pd.DataFrame( + filtered_adata.X, + columns=filtered_adata.var.index, + index=filtered_adata.obs.index + ) + print(dataframe.describe()) + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "transform_output.pickle") + # Default to pickle format if no recognized extension + if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): + output_file = output_file + '.pickle' + + saved_files = save_outputs({output_file: filtered_adata}) + + print(f"Subset Analysis completed → {saved_files[output_file]}") + return saved_files + else: + # Return the adata object directly for in-memory workflows + print("Returning AnnData object (not saving to file)") + return filtered_adata + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python subset_analysis_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned AnnData object") \ No newline at end of file diff --git a/tests/templates/test_subset_analysis_template.py b/tests/templates/test_subset_analysis_template.py new file mode 100644 index 00000000..b4e5db38 --- /dev/null +++ b/tests/templates/test_subset_analysis_template.py @@ -0,0 +1,148 @@ +# tests/templates/test_subset_analysis_template.py +"""Unit tests for the Subset Analysis template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.subset_analysis_template import run_from_json + + +def mock_adata(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + + # Create cell labels that match the example + cell_labels = ( + ["Normal_Cells", "Cancer_Cells"] * ((n_cells + 1) // 2) + )[:n_cells] + + obs = pd.DataFrame({ + "cell_labels": cell_labels, + "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] + }) + + x_mat = rng.normal(size=(n_cells, 3)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = ["Gene1", "Gene2", "Gene3"] + return adata + + +class TestSubsetAnalysisTemplate(unittest.TestCase): + """Unit tests for the Subset Analysis template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "output" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters from JSON template + self.params = { + "Upstream_Analysis": self.in_file, + "Annotation_of_interest": "cell_labels", + "Labels": ["Normal_Cells", "Cancer_Cells"], + "Include_Exclude": "Exclude Selected Labels", + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with exclude parameters (default from setup) + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + self.assertTrue(len(result) > 0) + + # Test 2: Run without saving + result_no_save = run_from_json(self.params, save_results=False) + # Check appropriate return type based on template + self.assertIsInstance(result_no_save, ad.AnnData) + # Verify that cells were actually filtered + original_adata = mock_adata() + self.assertLess(len(result_no_save), len(original_adata)) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test with non-existent annotation + params_bad = self.params.copy() + params_bad["Annotation_of_interest"] = "non_existent_annotation" + + # This should raise an error when select_values tries to access + # the annotation + with self.assertRaises((KeyError, ValueError)): + run_from_json(params_bad) + + @patch('spac.templates.subset_analysis_template.select_values') + def test_function_calls(self, mock_select) -> None: + """Test that main function is called with correct parameters.""" + # Mock the select_values function to return filtered data + filtered_adata = mock_adata(5) # Smaller filtered dataset + mock_select.return_value = filtered_adata + + # Test with "Include Selected Labels" + params_include = self.params.copy() + params_include["Include_Exclude"] = "Include Selected Labels" + + run_from_json(params_include, save_results=False) + + # Verify function was called correctly + mock_select.assert_called_once() + call_args = mock_select.call_args + + # Check that include mode sets values correctly + self.assertEqual(call_args[1]['annotation'], "cell_labels") + self.assertEqual( + call_args[1]['values'], ["Normal_Cells", "Cancer_Cells"] + ) + self.assertIsNone(call_args[1]['exclude_values']) + + # Reset mock + mock_select.reset_mock() + + # Test with "Exclude Selected Labels" + run_from_json(self.params, save_results=False) + + # Check that exclude mode sets values correctly + call_args = mock_select.call_args + self.assertIsNone(call_args[1]['values']) + self.assertEqual( + call_args[1]['exclude_values'], + ["Normal_Cells", "Cancer_Cells"] + ) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From aefcb294f3d3ab5059e76232c667b5c3a37963a1 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:30:01 -0400 Subject: [PATCH 073/102] fix(subset_analysis_template): fix typo and enhance function --- src/spac/templates/subset_analysis_template.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spac/templates/subset_analysis_template.py b/src/spac/templates/subset_analysis_template.py index b6242e20..915501d2 100644 --- a/src/spac/templates/subset_analysis_template.py +++ b/src/spac/templates/subset_analysis_template.py @@ -61,9 +61,9 @@ def run_from_json( labels = params["Labels"] # Use .get() with defaults for optional parameters from JSON template - toogle = params.get("Include_Exclude", "Include Selected Labels") + toggle = params.get("Include_Exclude", "Include Selected Labels") - if toogle == "Include Selected Labels": + if toggle == "Include Selected Labels": values_to_include = labels values_to_exclude = None else: @@ -77,7 +77,7 @@ def run_from_json( values=values_to_include, exclude_values=values_to_exclude ) - if caught_warnings is not None: + if caught_warnings: for warning in caught_warnings: raise ValueError(warning.message) From 542f985f4d1a2811f010b68da8dadffe8ac64220 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:38:04 -0400 Subject: [PATCH 074/102] feat(quantile_scaling_template): refactor nidap code, add quantile_scaling_template function and unit tests --- .../templates/quantile_scaling_template.py | 262 ++++++++++++++++++ .../test_quantile_scaling_template.py | 210 ++++++++++++++ 2 files changed, 472 insertions(+) create mode 100644 src/spac/templates/quantile_scaling_template.py create mode 100644 tests/templates/test_quantile_scaling_template.py diff --git a/src/spac/templates/quantile_scaling_template.py b/src/spac/templates/quantile_scaling_template.py new file mode 100644 index 00000000..2bd04e2f --- /dev/null +++ b/src/spac/templates/quantile_scaling_template.py @@ -0,0 +1,262 @@ +""" +Platform-agnostic Quantile Scaling template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.quantile_scaling_template import run_from_json +>>> run_from_json("examples/quantile_scaling_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, Tuple +import pandas as pd +import plotly.graph_objects as go + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.transformations import normalize_features +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True, + show_plot: bool = True +) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: + """ + Execute Quantile Scaling analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the adata object + and figure directly for in-memory workflows. Default is True. + show_plot : bool, optional + Whether to display the plot. Default is True. + + Returns + ------- + dict or tuple + If save_results=True: Dictionary of saved file paths + If save_results=False: Tuple of (adata, figure) + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters using .get() with defaults from JSON template + low_quantile = params.get("Low_Quantile", "0.02") + high_quantile = params.get("High_Quantile", "0.98") + interpolation = params.get("Interpolation", "nearest") + input_layer = params.get("Table_to_Process", "Original") + output_layer = params.get("Output_Table_Name", "normalized_feature") + per_batch = params.get("Per_Batch", "False") + # Annotation may be None, '', 'None', or a real name + annotation = params.get("Annotation") + + # Convert parameters using text_to_value + if input_layer == "Original": + input_layer = None + + low_quantile = text_to_value( + low_quantile, + to_float=True, + param_name='Low_Quantile' + ) + + high_quantile = text_to_value( + high_quantile, + to_float=True, + param_name='High_Quantile' + ) + + # Convert "True"/"False" string to boolean (case-insensitive) + per_batch = str(per_batch).strip().lower() == "true" + + # Annotation is optional - empty string or "None" becomes None + annotation = text_to_value(annotation) + + # Validate annotation is provided when per_batch is True + if per_batch and annotation is None: + raise ValueError( + 'Parameter "Annotation" is required when "Per Batch" is set ' + 'to True.' + ) + + # Check if output_layer already exists in adata + print(f"Checking if output layer '{output_layer}' exists in adata " + f"layers...") + if output_layer in adata.layers.keys(): + raise ValueError( + f"Output Table Name '{output_layer}' already exists, " + f"please rename it." + ) + else: + print(f"Output layer '{output_layer}' does not exist. " + f"Proceeding with normalization.") + + def df_as_html( + df, + columns_to_plot, + font_size=12, + column_scaler=1 + ): + df = df.reset_index() + df = df[columns_to_plot] + df_str = df.astype(str) + + column_widths = [ + max(df_str[col].apply(len)) * font_size * column_scaler + for col in df.columns + ] + column_widths[0] = 200 + + fig_width = sum(column_widths) * 1.1 + # Create a table trace with the DataFrame data + table_trace = go.Table( + header=dict(values=list(df.columns), + font=dict(size=font_size)), + cells=dict(values=df_str.values.T, + font=dict(size=font_size), + align='left'), + columnwidth=column_widths + ) + + layout = go.Layout( + autosize=True + ) + + fig = go.Figure( + data=[table_trace], + layout=layout + ) + + return fig + + def create_normalization_info( + adata, + low_quantile, + high_quantile, + input_layer, + output_layer + ): + pre_dataframe = adata.to_df(layer=input_layer) + quantiles = pre_dataframe.quantile([low_quantile, high_quantile]) + new_row_names = { + high_quantile: 'quantile_high', + low_quantile: 'quantile_low' + } + quantiles.index = quantiles.index.map(new_row_names) + + pre_info = pre_dataframe.describe() + pre_info = pd.concat([pre_info, quantiles]) + pre_info = pre_info.reset_index() + pre_info['index'] = 'Pre-Norm: ' + pre_info['index'].astype(str) + del pre_dataframe + + post_dataframe = adata.to_df(layer=output_layer) + post_info = post_dataframe.describe() + post_info = post_info.reset_index() + post_info['index'] = 'Post-Norm: ' + post_info['index'].astype(str) + del post_dataframe + + normalization_info = pd.concat([pre_info, post_info]).transpose() + normalization_info.columns = normalization_info.iloc[0] + normalization_info = normalization_info.drop( + normalization_info.index[0] + ) + normalization_info = normalization_info.astype(float) + normalization_info = normalization_info.round(3) + normalization_info = normalization_info.astype(str) + + return normalization_info + + print(f"High qunatile used: {str(high_quantile)}") + print(f"Low qunatile used: {str(low_quantile)}") + + transformed_data = normalize_features( + adata=adata, + low_quantile=low_quantile, + high_quantile=high_quantile, + interpolation=interpolation, + input_layer=input_layer, + output_layer=output_layer, + per_batch=per_batch, + annotation=annotation + ) + + print(f"Transformed data stored in layer: {output_layer}") + dataframe = pd.DataFrame(transformed_data.layers[output_layer]) + print(dataframe.describe()) + + normalization_info = create_normalization_info( + adata, + low_quantile, + high_quantile, + input_layer, + output_layer + ) + + columns_to_plot = [ + 'index', 'Pre-Norm: mean', 'Pre-Norm: std', + 'Pre-Norm: quantile_high', 'Pre-Norm: quantile_low', + 'Post-Norm: mean', 'Post-Norm: std', + ] + + html_plot = df_as_html( + normalization_info, + columns_to_plot + ) + + if show_plot: + html_plot.show() + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "transform_output.pickle") + # Default to pickle format if no recognized extension + if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): + output_file = output_file + '.pickle' + + saved_files = save_outputs({output_file: transformed_data}) + + print(f"Quantile Scaling completed → {saved_files[output_file]}") + return saved_files + else: + # Return the adata object and figure directly for in-memory + # workflows + print("Returning AnnData object and figure (not saving to file)") + return transformed_data, html_plot + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python quantile_scaling_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned AnnData object and figure") \ No newline at end of file diff --git a/tests/templates/test_quantile_scaling_template.py b/tests/templates/test_quantile_scaling_template.py new file mode 100644 index 00000000..59d13add --- /dev/null +++ b/tests/templates/test_quantile_scaling_template.py @@ -0,0 +1,210 @@ +# tests/templates/test_quantile_scaling_template.py +"""Unit tests for the Quantile Scaling template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.quantile_scaling_template import run_from_json + + +def mock_adata(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + # Create expression data with variance for normalization + x_mat = rng.exponential(scale=5, size=(n_cells, 5)) + obs = pd.DataFrame({ + "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], + "batch": (["Batch1", "Batch2"] * ((n_cells + 1) // 2))[:n_cells] + }) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = [f"Marker{i}" for i in range(5)] + # Add an existing layer for testing + adata.layers["arcsinh"] = np.arcsinh(x_mat / 5) + return adata + + +class TestQuantileScalingTemplate(unittest.TestCase): + """Unit tests for the Quantile Scaling template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "output" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters from NIDAP template + self.params = { + "Upstream_Analysis": self.in_file, + "Table_to_Process": "arcsinh", + "Low_Quantile": "0.02", + "High_Quantile": "0.98", + "Interpolation": "nearest", + "Output_Table_Name": "scaled_arcsinh", + "Per_Batch": "False", + "Annotation": "", + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + @patch('spac.templates.quantile_scaling_template.normalize_features') + def test_complete_io_workflow(self, mock_normalize) -> None: + """Single I/O test covering input/output scenarios.""" + # Mock the normalize_features function + def mock_transform(adata, **kwargs): + # Simulate normalization by adding a layer + output_layer = kwargs['output_layer'] + input_layer = kwargs.get('input_layer') + data = adata.layers[input_layer] if input_layer else adata.X + # Simple quantile scaling simulation + adata.layers[output_layer] = ( + (data - np.min(data)) / (np.max(data) - np.min(data)) + ) + return adata + + mock_normalize.side_effect = mock_transform + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + # Verify file was saved + self.assertTrue(len(result) > 0) + pickle_files = [f for f in result.values() if '.pickle' in str(f)] + self.assertTrue(len(pickle_files) > 0) + + # Test 2: Run without saving + result_no_save = run_from_json( + self.params, save_results=False, show_plot=False + ) + # Check appropriate return type - should be tuple (adata, figure) + self.assertIsInstance(result_no_save, tuple) + self.assertEqual(len(result_no_save), 2) + adata_result, fig_result = result_no_save + self.assertIsInstance(adata_result, ad.AnnData) + self.assertIn("scaled_arcsinh", adata_result.layers) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path, show_plot=False) + self.assertIsInstance(result_json, dict) + + # Verify normalize_features was called with correct parameters + call_args = mock_normalize.call_args + self.assertEqual(call_args[1]['input_layer'], "arcsinh") + self.assertEqual(call_args[1]['low_quantile'], 0.02) + self.assertEqual(call_args[1]['high_quantile'], 0.98) + self.assertEqual(call_args[1]['interpolation'], "nearest") + self.assertEqual(call_args[1]['output_layer'], "scaled_arcsinh") + self.assertEqual(call_args[1]['per_batch'], False) + # Empty string becomes None + self.assertIsNone(call_args[1]['annotation']) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test 1: output layer already exists + # First create adata with existing layer + adata = mock_adata() + adata.layers["scaled_arcsinh"] = adata.X.copy() + + with open(self.in_file, 'wb') as f: + pickle.dump(adata, f) + + with self.assertRaises(ValueError) as context: + run_from_json(self.params) + + # Check exact error message + expected_msg = ( + "Output Table Name 'scaled_arcsinh' already exists, " + "please rename it." + ) + self.assertEqual(str(context.exception), expected_msg) + + # Test 2: Missing annotation when per_batch is True + # Reset the input file + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + params_bad = self.params.copy() + params_bad["Per_Batch"] = "True" + params_bad["Annotation"] = "" # Empty annotation + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + expected_msg = ( + 'Parameter "Annotation" is required when "Per Batch" is set ' + 'to True.' + ) + self.assertEqual(str(context.exception), expected_msg) + + @patch('spac.templates.quantile_scaling_template.normalize_features') + @patch('builtins.print') + def test_console_output(self, mock_print, mock_normalize) -> None: + """Test that correct messages are printed.""" + # Mock the normalize function to return adata with the new layer + def mock_transform(adata, **kwargs): + output_layer = kwargs['output_layer'] + # Add the output layer + adata.layers[output_layer] = np.ones( + (adata.n_obs, adata.n_vars) + ) + return adata + + mock_normalize.side_effect = mock_transform + + # Test with different quantile values + params_alt = self.params.copy() + params_alt["Low_Quantile"] = "0.05" + params_alt["High_Quantile"] = "0.95" + params_alt["Per_Batch"] = "True" + params_alt["Annotation"] = "batch" + + run_from_json(params_alt, save_results=False, show_plot=False) + + # Check print statements + print_calls = [str(call[0][0]) for call in mock_print.call_args_list + if call[0]] + + # Should print quantile values (note typo in original) + self.assertTrue( + any("High qunatile used: 0.95" in msg for msg in print_calls) + ) + self.assertTrue( + any("Low qunatile used: 0.05" in msg for msg in print_calls) + ) + + # Verify function was called with per_batch=True + call_args = mock_normalize.call_args + self.assertEqual(call_args[1]['per_batch'], True) + self.assertEqual(call_args[1]['annotation'], "batch") + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From de6ee910c5a36eb7fa2c6429f36695317ceebb03 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:46:30 -0400 Subject: [PATCH 075/102] fix(quantile_scaling_template): fix typo in both function and unit tests --- src/spac/templates/quantile_scaling_template.py | 4 ++-- tests/templates/test_quantile_scaling_template.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/spac/templates/quantile_scaling_template.py b/src/spac/templates/quantile_scaling_template.py index 2bd04e2f..205df31c 100644 --- a/src/spac/templates/quantile_scaling_template.py +++ b/src/spac/templates/quantile_scaling_template.py @@ -184,8 +184,8 @@ def create_normalization_info( return normalization_info - print(f"High qunatile used: {str(high_quantile)}") - print(f"Low qunatile used: {str(low_quantile)}") + print(f"High quantile used: {str(high_quantile)}") + print(f"Low quantile used: {str(low_quantile)}") transformed_data = normalize_features( adata=adata, diff --git a/tests/templates/test_quantile_scaling_template.py b/tests/templates/test_quantile_scaling_template.py index 59d13add..814bcc65 100644 --- a/tests/templates/test_quantile_scaling_template.py +++ b/tests/templates/test_quantile_scaling_template.py @@ -192,12 +192,12 @@ def mock_transform(adata, **kwargs): print_calls = [str(call[0][0]) for call in mock_print.call_args_list if call[0]] - # Should print quantile values (note typo in original) + # Should print quantile values self.assertTrue( - any("High qunatile used: 0.95" in msg for msg in print_calls) + any("High quantile used: 0.95" in msg for msg in print_calls) ) self.assertTrue( - any("Low qunatile used: 0.05" in msg for msg in print_calls) + any("Low quantile used: 0.05" in msg for msg in print_calls) ) # Verify function was called with per_batch=True From a71e8656033f79736330a1f902200a6c81c4b37e Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 13:12:25 -0400 Subject: [PATCH 076/102] feat(normalize_batch_template): add normalize_batch_template functionand unit tests --- .../templates/normalize_batch_template.py | 119 +++++++++++++ .../test_normalize_batch_template.py | 160 ++++++++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 src/spac/templates/normalize_batch_template.py create mode 100644 tests/templates/test_normalize_batch_template.py diff --git a/src/spac/templates/normalize_batch_template.py b/src/spac/templates/normalize_batch_template.py new file mode 100644 index 00000000..f74adb00 --- /dev/null +++ b/src/spac/templates/normalize_batch_template.py @@ -0,0 +1,119 @@ +""" +Platform-agnostic Normalize Batch template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.normalize_batch_template import run_from_json +>>> run_from_json("examples/normalize_batch_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union +import pandas as pd + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.transformations import batch_normalize +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], Any]: + """ + Execute Normalize Batch analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the adata object + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or AnnData + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed AnnData object + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + all_data = load_input(params["Upstream_Analysis"]) + + # Extract parameters + annotation = params["Annotation"] + input_layer = params.get("Input_Table_Name", "Original") + + if input_layer == 'Original': + input_layer = None + + output_layer = params.get("Output_Table_Name", "batch_normalized_table") + method = params.get("Normalization_Method", "median") + take_log = params.get("Take_Log", False) + + need_normalization = params.get("Need_Normalization", False) + if need_normalization: + batch_normalize( + adata=all_data, + annotation=annotation, + input_layer=input_layer, + output_layer=output_layer, + method=method, + log=take_log + ) + + print("Statistics of original data:\n", all_data.to_df().describe()) + print("Statistics of layer data:\n", all_data.to_df(layer=output_layer).describe()) + else: + print("Statistics of original data:\n", all_data.to_df().describe()) + + print("Current Analysis contains:\n", all_data) + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "transform_output.pickle") + # Default to pickle format if no recognized extension + if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): + output_file = output_file + '.pickle' + + saved_files = save_outputs({output_file: all_data}) + + print(f"Normalize Batch completed → {saved_files[output_file]}") + return saved_files + else: + # Return the adata object directly for in-memory workflows + print("Returning AnnData object (not saving to file)") + return all_data + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python normalize_batch_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned AnnData object") \ No newline at end of file diff --git a/tests/templates/test_normalize_batch_template.py b/tests/templates/test_normalize_batch_template.py new file mode 100644 index 00000000..66ade9b7 --- /dev/null +++ b/tests/templates/test_normalize_batch_template.py @@ -0,0 +1,160 @@ +# tests/templates/test_normalize_batch_template.py +"""Unit tests for the Normalize Batch template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.normalize_batch_template import run_from_json + + +def mock_adata(n_cells: int = 10) -> ad.AnnData: + """Return a minimal synthetic AnnData for fast tests.""" + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "batch": (["Batch1", "Batch2"] * ((n_cells + 1) // 2))[:n_cells], + "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] + }) + x_mat = rng.normal(size=(n_cells, 3)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = ["Gene1", "Gene2", "Gene3"] + # Add an empty layers dict + adata.layers = {} + return adata + + +class TestNormalizeBatchTemplate(unittest.TestCase): + """Unit tests for the Normalize Batch template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "output" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(), f) + + # Minimal parameters from JSON template + self.params = { + "Upstream_Analysis": self.in_file, + "Need_Normalization": False, + "Input_Table_Name": "Original", + "Output_Table_Name": "batch_normalized_table", + "Annotation": "batch", + "Normalization_Method": "median", + "Take_Log": False, + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + @patch('spac.templates.normalize_batch_template.batch_normalize') + def test_complete_io_workflow(self, mock_batch_normalize) -> None: + """Single I/O test covering input/output scenarios.""" + # Mock the batch_normalize function + def mock_normalize(adata, **kwargs): + # Simulate normalization by adding a layer + adata.layers[kwargs['output_layer']] = adata.X.copy() + return adata + + mock_batch_normalize.side_effect = mock_normalize + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with Need_Normalization=False (no normalization) + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + self.assertTrue(len(result) > 0) + # Verify batch_normalize was NOT called + mock_batch_normalize.assert_not_called() + + # Test 2: Run with Need_Normalization=True + params_norm = self.params.copy() + params_norm["Need_Normalization"] = True + + result_norm = run_from_json(params_norm) + self.assertIsInstance(result_norm, dict) + # Verify batch_normalize WAS called + mock_batch_normalize.assert_called_once() + + # Test 3: Run without saving + result_no_save = run_from_json(params_norm, save_results=False) + self.assertIsInstance(result_no_save, ad.AnnData) + + # Test 4: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + @patch('spac.templates.normalize_batch_template.batch_normalize') + def test_function_calls(self, mock_batch_normalize) -> None: + """Test that main function is called with correct parameters.""" + # Mock the batch_normalize function to modify adata in-place + def mock_normalize(adata, **kwargs): + # Simulate normalization by adding a layer + output_layer = kwargs.get('output_layer', 'batch_normalized_table') + adata.layers[output_layer] = adata.X.copy() + # batch_normalize modifies in-place, returns None + return None + + mock_batch_normalize.side_effect = mock_normalize + + # Test with normalization enabled + params_enabled = self.params.copy() + params_enabled["Need_Normalization"] = True + params_enabled["Normalization_Method"] = "z-score" + params_enabled["Take_Log"] = True + + run_from_json(params_enabled, save_results=False) + + # Verify function was called correctly + mock_batch_normalize.assert_called_once() + call_args = mock_batch_normalize.call_args + + # Check specific parameter conversions + self.assertEqual(call_args[1]['annotation'], "batch") + # "Original" → None + self.assertEqual(call_args[1]['input_layer'], None) + self.assertEqual( + call_args[1]['output_layer'], "batch_normalized_table" + ) + self.assertEqual(call_args[1]['method'], "z-score") + self.assertEqual(call_args[1]['log'], True) + + def test_parameter_defaults(self) -> None: + """Test that default parameters work correctly.""" + # Minimal required parameters only + params_minimal = { + "Upstream_Analysis": self.in_file, + "Annotation": "batch" + } + + with patch('spac.templates.normalize_batch_template.batch_normalize'): + result = run_from_json(params_minimal) + self.assertIsInstance(result, dict) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 9edf8cee2972ecfd34244d7f3482a7ca5be94b2e Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 13:29:14 -0400 Subject: [PATCH 077/102] fix(normalize_batch_template): fix typo and unused import --- src/spac/templates/normalize_batch_template.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/spac/templates/normalize_batch_template.py b/src/spac/templates/normalize_batch_template.py index f74adb00..aa746396 100644 --- a/src/spac/templates/normalize_batch_template.py +++ b/src/spac/templates/normalize_batch_template.py @@ -11,7 +11,6 @@ import sys from pathlib import Path from typing import Any, Dict, Union -import pandas as pd # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) @@ -21,7 +20,6 @@ load_input, save_outputs, parse_params, - text_to_value, ) From abda61091b2a94251eccbe2535efc300d79a7e73 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 13:58:48 -0400 Subject: [PATCH 078/102] feat(tsne_analysis_template): add tsne_analysis_template function and unit tests --- src/spac/templates/tsne_analysis_template.py | 108 ++++++++++++++ .../templates/test_tsne_analysis_template.py | 135 ++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 src/spac/templates/tsne_analysis_template.py create mode 100644 tests/templates/test_tsne_analysis_template.py diff --git a/src/spac/templates/tsne_analysis_template.py b/src/spac/templates/tsne_analysis_template.py new file mode 100644 index 00000000..8456ef1e --- /dev/null +++ b/src/spac/templates/tsne_analysis_template.py @@ -0,0 +1,108 @@ +""" +Platform-agnostic tSNE Analysis template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.tsne_analysis_template import run_from_json +>>> run_from_json("examples/tsne_analysis_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.transformations import tsne +from spac.templates.template_utils import ( + load_input, + save_outputs, + parse_params, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True +) -> Union[Dict[str, str], Any]: + """ + Execute tSNE Analysis analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the adata object + directly for in-memory workflows. Default is True. + + Returns + ------- + dict or AnnData + If save_results=True: Dictionary of saved file paths + If save_results=False: The processed AnnData object + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + all_data = load_input(params["Upstream_Analysis"]) + + # Extract parameters + # Select layer to perform tSNHE + Layer_to_Analysis = params.get("Table_to_Process", "Original") + + print(all_data) + if Layer_to_Analysis == "Original": + Layer_to_Analysis = None + + print("tSNE Layer: \n", Layer_to_Analysis) + + print("Performing tSNE ...") + + tsne(all_data, layer=Layer_to_Analysis) + + print("tSNE Done!") + + print(all_data) + + object_to_output = all_data + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "transform_output.pickle") + # Default to pickle format if no recognized extension + if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): + output_file = output_file + '.pickle' + + saved_files = save_outputs({output_file: object_to_output}) + + print(f"tSNE Analysis completed → {saved_files[output_file]}") + return saved_files + else: + # Return the adata object directly for in-memory workflows + print("Returning AnnData object (not saving to file)") + return object_to_output + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python tsne_analysis_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned AnnData object") \ No newline at end of file diff --git a/tests/templates/test_tsne_analysis_template.py b/tests/templates/test_tsne_analysis_template.py new file mode 100644 index 00000000..7694f191 --- /dev/null +++ b/tests/templates/test_tsne_analysis_template.py @@ -0,0 +1,135 @@ +# tests/templates/test_tsne_analysis_template.py +"""Unit tests for the tSNE Analysis template.""" + +import json +import os +import pickle +import sys +import tempfile +import unittest +import warnings +from unittest.mock import patch, MagicMock + +import anndata as ad +import numpy as np +import pandas as pd +from pathlib import Path + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.tsne_analysis_template import run_from_json + + +def mock_adata(n_cells: int = 100) -> ad.AnnData: + """ + Return a minimal synthetic AnnData for fast tests. + + Note: tSNE requires n_cells > perplexity (default 30), + so we use 100 cells by default. + """ + rng = np.random.default_rng(0) + obs = pd.DataFrame({ + "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], + "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] + }) + x_mat = rng.normal(size=(n_cells, 10)) + adata = ad.AnnData(X=x_mat, obs=obs) + adata.var_names = [f"Gene{i}" for i in range(10)] + # Add spatial coordinates if needed + adata.obsm["spatial"] = rng.random((n_cells, 2)) * 100 + return adata + + +class TestTsneAnalysisTemplate(unittest.TestCase): + """Unit tests for the tSNE Analysis template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.in_file = os.path.join( + self.tmp_dir.name, "input.pickle" + ) + self.out_file = "output" + + # Save minimal mock data + with open(self.in_file, 'wb') as f: + pickle.dump(mock_adata(100), f) + + # Minimal parameters - adjust based on template + self.params = { + "Upstream_Analysis": self.in_file, + "Table_to_Process": "Original", + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + # Verify a pickle file was created + self.assertTrue( + any('.pickle' in str(f) for f in result.values()) + ) + + # Test 2: Run without saving + result_no_save = run_from_json(self.params, save_results=False) + # Check appropriate return type based on template + self.assertIsInstance(result_no_save, ad.AnnData) + # Verify that tSNE was added to obsm + self.assertIn("X_tsne", result_no_save.obsm) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + + def test_layer_parameter(self) -> None: + """Test that layer parameter is handled correctly.""" + # Create adata with a layer - use 100 cells for tSNE + adata = mock_adata(100) + adata.layers["normalized"] = adata.X * 2 + + with open(self.in_file, 'wb') as f: + pickle.dump(adata, f) + + # Test with specific layer + params_layer = self.params.copy() + params_layer["Table_to_Process"] = "normalized" + + result = run_from_json(params_layer, save_results=False) + self.assertIn("X_tsne", result.obsm) + + @patch('spac.templates.tsne_analysis_template.tsne') + def test_function_calls(self, mock_tsne) -> None: + """Test that main function is called with correct parameters.""" + # Mock the tsne function + def mock_tsne_func(adata, layer=None): + # Simulate adding tSNE results + adata.obsm["X_tsne"] = np.random.rand(adata.n_obs, 2) + return adata + + mock_tsne.side_effect = mock_tsne_func + + run_from_json(self.params) + + # Verify function was called correctly + mock_tsne.assert_called_once() + call_args = mock_tsne.call_args + + # Check that layer=None when "Original" is specified + self.assertIsNone(call_args[1]['layer']) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 7ae8e5725326e68e9f2ce440be62089c06ad5f36 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:05:14 -0400 Subject: [PATCH 079/102] fix(tsne_analysis_template): fixed typo --- src/spac/templates/tsne_analysis_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spac/templates/tsne_analysis_template.py b/src/spac/templates/tsne_analysis_template.py index 8456ef1e..b2366bd7 100644 --- a/src/spac/templates/tsne_analysis_template.py +++ b/src/spac/templates/tsne_analysis_template.py @@ -52,7 +52,7 @@ def run_from_json( all_data = load_input(params["Upstream_Analysis"]) # Extract parameters - # Select layer to perform tSNHE + # Select layer to perform tSNE Layer_to_Analysis = params.get("Table_to_Process", "Original") print(all_data) From bbb53f712285a19f738aa117205e907c1aa0404d Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:27:38 -0400 Subject: [PATCH 080/102] feat(posit_it_python_template): add posit_it_python_template functionand unit tests --- .../templates/posit_it_python_template.py | 220 ++++++++++++++++++ .../test_posit_it_python_template.py | 130 +++++++++++ 2 files changed, 350 insertions(+) create mode 100644 src/spac/templates/posit_it_python_template.py create mode 100644 tests/templates/test_posit_it_python_template.py diff --git a/src/spac/templates/posit_it_python_template.py b/src/spac/templates/posit_it_python_template.py new file mode 100644 index 00000000..7f8c3290 --- /dev/null +++ b/src/spac/templates/posit_it_python_template.py @@ -0,0 +1,220 @@ +""" +Platform-agnostic Post-It-Python template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.posit_it_python_template import run_from_json +>>> run_from_json("examples/posit_it_python_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, Optional, Tuple +import matplotlib.pyplot as plt + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.templates.template_utils import ( + save_outputs, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_results: bool = True, + show_plot: bool = False +) -> Union[Dict[str, str], Any]: + """ + Execute Post-It-Python analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_results : bool, optional + Whether to save results to file. If False, returns the figure + directly for in-memory workflows. Default is True. + show_plot : bool, optional + Whether to display the plot. Default is False. + + Returns + ------- + dict or figure + If save_results=True: Dictionary of saved file paths + If save_results=False: The matplotlib figure object + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Extract parameters using .get() with defaults from JSON template + text = params.get("Label", "Post-It") + text_color = params.get("Label_font_color", "Black") + text_size = params.get("Label_font_size", "80") + text_fontface = params.get("Label_font_type", "normal") + text_fontfamily = params.get("Label_font_family", "Arial") + bold = params.get("Label_Bold", "False") + + # background params + fill_color = params.get("Background_fill_color", "Yellow1") + fill_alpha = params.get("Background_fill_opacity", "10") + border_alpha = 1 + border_width = 0 + + # image params + image_width = params.get("Page_width", "18") + image_height = params.get("Page_height", "6") + image_resolution = params.get("Page_DPI", "300") + + # output value + tag = "CCBR" + + # Convert string parameters to appropriate types + text_size = text_to_value( + text_size, + to_int=True, + param_name="Label_font_size" + ) + + bold = text_to_value(bold) == "True" + + fill_alpha = text_to_value( + fill_alpha, + to_float=True, + param_name="Background_fill_opacity" + ) + + image_width = text_to_value( + image_width, + to_float=True, + param_name="Page_width" + ) + + image_height = text_to_value( + image_height, + to_float=True, + param_name="Page_height" + ) + + image_resolution = text_to_value( + image_resolution, + to_int=True, + param_name="Page_DPI" + ) + + # RUN ==== + + # paints + paints = { + 'White': '#FFFFFF', + 'LightGrey': '#D3D3D3', + 'Grey': '#999999', + 'Black': '#000000', + 'Red1': '#F44E3B', + 'Red2': '#D33115', + 'Red3': '#9F0500', + 'Orange1': '#FE9200', + 'Orange2': '#E27300', + 'Orange3': '#C45100', + 'Yellow1': '#FCDC00', + 'Yellow2': '#FCC400', + 'Yellow3': '#FB9E00', + 'YellowGreen1': '#DBDF00', + 'YellowGreen2': '#B0BC00', + 'Yellowgreen3': '#808900', + 'Green1': '#A4DD00', + 'Green2': '#68BC00', + 'Green3': '#194D33', + 'Teal1': '#68CCCA', + 'Teal2': '#16A5A5', + 'Teal3': '#0C797D', + 'Blue1': '#73D8FF', + 'Blue2': '#009CE0', + 'Blue3': '#0062B1', + 'Purple1': '#AEA1FF', + 'Purple2': '#7B64FF', + 'Purple3': '#653294', + 'Magenta1': '#FDA1FF', + 'Magenta2': '#FA28FF', + 'Magenta3': '#AB149E' + } + + # image: png + fig = plt.figure( + figsize=(image_width, image_height), + dpi=image_resolution + ) + fig.patch.set_facecolor(paints[fill_color]) + fig.patch.set_alpha(fill_alpha/100) + for ax in fig.get_axes(): + for item in ([ax.title, ax.xaxis.label, ax.yaxis.label] + + ax.get_xticklabels() + ax.get_yticklabels()): + item.set_fontsize(text_size) + item.set_fontfamily(text_fontfamily) + item.set_fontstyle(text_fontface) + if bold: + item.set_fontweight('bold') + + # plt.set_facecolor(paints[fill_color]+hex_fill) + + fig.text( + 0.5, 0.5, text, + fontsize=text_size, + color=paints[text_color], + ha='center', + va='center', + fontfamily=text_fontfamily, + fontstyle=text_fontface, + fontweight='bold' if bold else 'normal' + ) + + if show_plot: + plt.show() + + # Handle results based on save_results flag + if save_results: + # Save outputs + output_file = params.get("Output_File", "graphicsFile.png") + # Ensure .png extension + if not output_file.endswith('.png'): + output_file = output_file + '.png' + + fig.savefig( + output_file, + format='png', + transparent=True, + bbox_inches='tight' + ) + plt.close(fig) + + saved_files = {output_file: output_file} + + print(f"Post-It-Python completed → {saved_files[output_file]}") + return saved_files + else: + # Return the figure object directly for in-memory workflows + print("Returning figure object (not saving to file)") + return fig + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) != 2: + print( + "Usage: python posit_it_python_template.py ", + file=sys.stderr + ) + sys.exit(1) + + result = run_from_json(sys.argv[1]) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned figure object") \ No newline at end of file diff --git a/tests/templates/test_posit_it_python_template.py b/tests/templates/test_posit_it_python_template.py new file mode 100644 index 00000000..d1dde346 --- /dev/null +++ b/tests/templates/test_posit_it_python_template.py @@ -0,0 +1,130 @@ +# tests/templates/test_posit_it_python_template.py +"""Unit tests for the Post-It-Python template.""" + +import json +import os +import sys +import tempfile +import unittest +import warnings +from pathlib import Path +from unittest.mock import patch, MagicMock + +import matplotlib +matplotlib.use("Agg") # Headless backend for CI +import matplotlib.pyplot as plt + +sys.path.append( + os.path.dirname(os.path.realpath(__file__)) + "/../../src" +) + +from spac.templates.posit_it_python_template import run_from_json + + +class TestPostItPythonTemplate(unittest.TestCase): + """Unit tests for the Post-It-Python template.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.TemporaryDirectory() + self.out_file = "graphicsFile.png" + + # Minimal parameters from NIDAP template + self.params = { + "Label": "Post-It", + "Label_font_size": "80", + "Label_font_type": "normal", + "Label_Bold": "False", + "Label_font_color": "Black", + "Label_font_family": "Arial", + "Background_fill_color": "Yellow1", + "Background_fill_opacity": "10", + "Page_width": "18", + "Page_height": "6", + "Page_DPI": "300", + "Output_File": self.out_file, + } + + def tearDown(self) -> None: + self.tmp_dir.cleanup() + # Clean up any matplotlib figures + plt.close('all') + + def test_complete_io_workflow(self) -> None: + """Single I/O test covering input/output scenarios.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Test 1: Run with default parameters + result = run_from_json(self.params) + self.assertIsInstance(result, dict) + self.assertIn(self.out_file, result) + # Check file was created + self.assertTrue(os.path.exists(result[self.out_file])) + # Clean up + os.remove(result[self.out_file]) + + # Test 2: Run without saving + result_no_save = run_from_json(self.params, save_results=False) + # Check appropriate return type - should be a figure + self.assertIsInstance(result_no_save, plt.Figure) + plt.close(result_no_save) + + # Test 3: JSON file input + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(self.params, f) + + result_json = run_from_json(json_path) + self.assertIsInstance(result_json, dict) + # Clean up + if os.path.exists(result_json[self.out_file]): + os.remove(result_json[self.out_file]) + + def test_error_validation(self) -> None: + """Test exact error message for invalid parameters.""" + # Test invalid integer conversion for font size + params_bad = self.params.copy() + params_bad["Label_font_size"] = "invalid_number" + + with self.assertRaises(ValueError) as context: + run_from_json(params_bad) + + # Check exact error message + expected_msg = ( + "Error: can't convert Label_font_size to integer. " + "Received:\"invalid_number\"" + ) + self.assertEqual(str(context.exception), expected_msg) + + def test_function_calls(self) -> None: + """Test that function is called with correct parameters.""" + # Test with custom text and colors + params_custom = self.params.copy() + params_custom["Label"] = "Test Label" + params_custom["Label_font_color"] = "Red1" + params_custom["Background_fill_color"] = "Blue1" + params_custom["Label_Bold"] = "True" + + result = run_from_json(params_custom, save_results=False) + + # Verify figure was created with correct properties + self.assertIsInstance(result, plt.Figure) + # Check figure size + self.assertEqual(result.get_figwidth(), 18.0) + self.assertEqual(result.get_figheight(), 6.0) + + plt.close(result) + + def test_minimal_params(self) -> None: + """Test with minimal parameters using defaults.""" + minimal_params = {} # All defaults from JSON + + result = run_from_json(minimal_params, save_results=False) + + # Should still create a valid figure + self.assertIsInstance(result, plt.Figure) + plt.close(result) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From e70f547aae17a011ce52162c0bf6fd42a74902ed Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:33:50 -0400 Subject: [PATCH 081/102] fix(posit_it_python_template): fixed typo --- src/spac/templates/posit_it_python_template.py | 6 ++---- tests/templates/test_posit_it_python_template.py | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/spac/templates/posit_it_python_template.py b/src/spac/templates/posit_it_python_template.py index 7f8c3290..716062d3 100644 --- a/src/spac/templates/posit_it_python_template.py +++ b/src/spac/templates/posit_it_python_template.py @@ -125,7 +125,7 @@ def run_from_json( 'Yellow3': '#FB9E00', 'YellowGreen1': '#DBDF00', 'YellowGreen2': '#B0BC00', - 'Yellowgreen3': '#808900', + 'YellowGreen3': '#808900', 'Green1': '#A4DD00', 'Green2': '#68BC00', 'Green3': '#194D33', @@ -157,9 +157,7 @@ def run_from_json( item.set_fontfamily(text_fontfamily) item.set_fontstyle(text_fontface) if bold: - item.set_fontweight('bold') - - # plt.set_facecolor(paints[fill_color]+hex_fill) + item.set_fontweight('bold') fig.text( 0.5, 0.5, text, diff --git a/tests/templates/test_posit_it_python_template.py b/tests/templates/test_posit_it_python_template.py index d1dde346..926d57f1 100644 --- a/tests/templates/test_posit_it_python_template.py +++ b/tests/templates/test_posit_it_python_template.py @@ -1,5 +1,5 @@ # tests/templates/test_posit_it_python_template.py -"""Unit tests for the Post-It-Python template.""" +"""Unit tests for the Posit-It-Python template.""" import json import os @@ -22,7 +22,7 @@ class TestPostItPythonTemplate(unittest.TestCase): - """Unit tests for the Post-It-Python template.""" + """Unit tests for the Posit-It-Python template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() From 9e3bea0de6400b3a1031f831f8ced81f410b9007 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Wed, 1 Oct 2025 03:04:36 -0400 Subject: [PATCH 082/102] feat: Add SPAC boxplot Galaxy tool for Docker deployment --- galaxy_tools/README.md | 12 + .../spac_boxplot/run_spac_template.sh | 391 ++++++++++++++++++ galaxy_tools/spac_boxplot/spac_boxplot.xml | 76 ++++ galaxy_tools/test-data/setup_analysis.h5ad | Bin 0 -> 1552282 bytes galaxy_tools/test-data/setup_analysis.pickle | Bin 0 -> 1366162 bytes 5 files changed, 479 insertions(+) create mode 100644 galaxy_tools/README.md create mode 100644 galaxy_tools/spac_boxplot/run_spac_template.sh create mode 100644 galaxy_tools/spac_boxplot/spac_boxplot.xml create mode 100644 galaxy_tools/test-data/setup_analysis.h5ad create mode 100644 galaxy_tools/test-data/setup_analysis.pickle diff --git a/galaxy_tools/README.md b/galaxy_tools/README.md new file mode 100644 index 00000000..c615436a --- /dev/null +++ b/galaxy_tools/README.md @@ -0,0 +1,12 @@ +# SPAC Galaxy Tools + + ## Requirements + - Galaxy instance with Docker enabled + - Docker image: nciccbr/spac:v1 + + ## Installation + 1. Pull Docker image: `docker pull nciccbr/spac:v1` + 2. Copy tool directory to Galaxy's tools folder + 3. Add to tool_conf.xml: +```xml + \ No newline at end of file diff --git a/galaxy_tools/spac_boxplot/run_spac_template.sh b/galaxy_tools/spac_boxplot/run_spac_template.sh new file mode 100644 index 00000000..f2c32068 --- /dev/null +++ b/galaxy_tools/spac_boxplot/run_spac_template.sh @@ -0,0 +1,391 @@ +#!/usr/bin/env bash +# run_spac_template.sh - Docker version for Galaxy +# Version: 4.0.0 - Imports templates from installed SPAC package +set -euo pipefail + +# Log everything to tool_stdout.txt +exec > >(tee -a tool_stdout.txt) 2>&1 + +PARAMS_JSON="${1:?Missing params.json path}" +TEMPLATE_NAME="${2:?Missing template name}" # Just the name, not filename + +# Use system Python inside Docker container +SPAC_PYTHON="${SPAC_PYTHON:-python3}" + +echo "=== SPAC Template Wrapper v4.0 (Docker) ===" +echo "Parameters: $PARAMS_JSON" +echo "Template: $TEMPLATE_NAME" +echo "Python: $SPAC_PYTHON" +echo "Working directory: $(pwd)" + +# Run template through Python interpreter +"$SPAC_PYTHON" - <<'PYTHON_RUNNER' "$PARAMS_JSON" "$TEMPLATE_NAME" +import json +import os +import sys +import copy +import importlib +import traceback +import inspect + +# Get command line arguments +params_path = sys.argv[1] +template_name = sys.argv[2] # Just the name like "boxplot", not "boxplot_template.py" + +print(f"[Runner] Starting execution for template: {template_name}") +print(f"[Runner] Python version: {sys.version}") + +# ============================================================================ +# HELPER FUNCTIONS +# ============================================================================ + +def determine_outputs_from_params(params): + """Read outputs from params if available, otherwise use defaults""" + if 'outputs' in params and isinstance(params['outputs'], dict): + outputs = params['outputs'] + print(f"[Runner] Using outputs from params: {list(outputs.keys())}") + return outputs + + # Fallback to defaults based on template name + print(f"[Runner] No outputs in params, using defaults for {template_name}") + + if template_name in ['boxplot', 'histogram', 'violin_plot', 'scatter_plot', + 'hierarchical_heatmap', 'relational_heatmap']: + return {'figures': 'figure_folder', 'DataFrames': 'dataframe_folder'} + elif template_name in ['interactive_spatial_plot', 'interactive_scatter_plot']: + return {'html': 'html_folder'} + elif template_name in ['analysis_to_csv', 'select_values']: + return {'DataFrames': 'dataframe_folder'} + elif template_name == 'setup_analysis': + return {'analysis': 'analysis_output.pickle'} + else: + return {'analysis': 'transform_output.pickle'} + +def inject_output_paths(params, outputs, template_name): + """Add output paths to parameters""" + params_exec = copy.deepcopy(params) + + # Remove the 'outputs' field before execution + params_exec.pop('outputs', None) + + params_exec['save_results'] = True + + if 'analysis' in outputs: + params_exec['output_path'] = outputs['analysis'] + params_exec['Output_Path'] = outputs['analysis'] + params_exec['Output_File'] = outputs['analysis'] + + if 'DataFrames' in outputs: + df_dir = outputs['DataFrames'] + params_exec['output_dir'] = df_dir + params_exec['Export_Dir'] = df_dir + params_exec['Output_File'] = os.path.join(df_dir, f'{template_name}_output.csv') + + if 'figures' in outputs: + fig_dir = outputs['figures'] + params_exec['figure_dir'] = fig_dir + params_exec['Figure_Dir'] = fig_dir + params_exec['Figure_File'] = os.path.join(fig_dir, f'{template_name}.png') + + if 'html' in outputs: + html_dir = outputs['html'] + params_exec['output_path'] = os.path.join(html_dir, f'{template_name}.html') + params_exec['Output_File'] = params_exec['output_path'] + + return params_exec + +def handle_template_results(result, outputs, template_name): + """Save any in-memory results returned by the template""" + if result is None: + return + + saved_count = 0 + + try: + import pandas as pd + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + + if isinstance(result, tuple): + for i, item in enumerate(result): + if hasattr(item, 'savefig') and 'figures' in outputs: + fig_path = os.path.join(outputs['figures'], f'{template_name}_{i+1}.png') + item.savefig(fig_path, dpi=300, bbox_inches='tight') + print(f"[Runner] Saved figure to {fig_path}") + plt.close(item) + saved_count += 1 + elif hasattr(item, 'to_csv') and 'DataFrames' in outputs: + csv_path = os.path.join(outputs['DataFrames'], f'{template_name}_{i+1}.csv') + item.to_csv(csv_path, index=True) + print(f"[Runner] Saved DataFrame to {csv_path}") + saved_count += 1 + + elif hasattr(result, 'to_csv') and 'DataFrames' in outputs: + csv_path = os.path.join(outputs['DataFrames'], f'{template_name}_output.csv') + result.to_csv(csv_path, index=True) + print(f"[Runner] Saved DataFrame to {csv_path}") + saved_count += 1 + + elif hasattr(result, 'savefig') and 'figures' in outputs: + fig_path = os.path.join(outputs['figures'], f'{template_name}.png') + result.savefig(fig_path, dpi=300, bbox_inches='tight') + print(f"[Runner] Saved figure to {fig_path}") + plt.close(result) + saved_count += 1 + + elif hasattr(result, 'write_h5ad') and 'analysis' in outputs: + result.write_h5ad(outputs['analysis']) + print(f"[Runner] Saved AnnData to {outputs['analysis']}") + saved_count += 1 + + except ImportError as e: + print(f"[Runner] Note: Some libraries not available for result handling: {e}") + + if saved_count > 0: + print(f"[Runner] Saved {saved_count} in-memory results") + +def verify_outputs(outputs): + """Verify that expected outputs were created""" + found_outputs = False + + for output_type, path in outputs.items(): + if output_type == 'analysis': + if os.path.exists(path): + size = os.path.getsize(path) + print(f"[Runner] ✓ {output_type}: {os.path.basename(path)} ({size:,} bytes)") + found_outputs = True + else: + print(f"[Runner] ✗ {output_type}: NOT FOUND at {path}") + else: + if os.path.exists(path) and os.path.isdir(path): + files = os.listdir(path) + if files: + total_size = sum(os.path.getsize(os.path.join(path, f)) for f in files) + print(f"[Runner] ✓ {output_type}: {len(files)} files ({total_size:,} bytes)") + for f in files[:3]: + size = os.path.getsize(os.path.join(path, f)) + print(f"[Runner] - {f} ({size:,} bytes)") + if len(files) > 3: + print(f"[Runner] ... and {len(files)-3} more files") + found_outputs = True + else: + print(f"[Runner] ⚠ {output_type}: directory exists but empty") + + if not found_outputs: + print("[Runner] WARNING: No outputs were created!") + +# ============================================================================ +# PARAMETER PROCESSING +# ============================================================================ + +def _unsanitize(s: str) -> str: + """Remove Galaxy's parameter sanitization tokens""" + replacements = { + '__ob__': '[', '__cb__': ']', + '__oc__': '{', '__cc__': '}', + '__dq__': '"', '__sq__': "'", + '__gt__': '>', '__lt__': '<', + '__cn__': '\n', '__cr__': '\r', + '__tc__': '\t', '__pd__': '#', + '__at__': '@', '__cm__': ',', + '__dollar__': '$', '__us__': '_' + } + for token, char in replacements.items(): + s = s.replace(token, char) + return s + +def _maybe_parse(v): + """Recursively unsanitize and parse JSON where possible""" + if isinstance(v, str): + u = _unsanitize(v).strip() + if (u.startswith('[') and u.endswith(']')) or (u.startswith('{') and u.endswith('}')): + try: + return json.loads(u) + except: + return u + return u + elif isinstance(v, dict): + return {k: _maybe_parse(val) for k, val in v.items()} + elif isinstance(v, list): + return [_maybe_parse(item) for item in v] + return v + +def should_normalize_as_list(key, value): + """Determine if a parameter should be normalized as a list""" + if isinstance(value, list): + return True + + if value is None or value == "": + return False + + key_lower = key.lower() + + if 'regex' in key_lower or 'pattern' in key_lower: + return False + + if any(x in key_lower for x in ['single', 'one', 'first', 'second', 'primary']): + return False + + if any(x in key_lower for x in ['features', 'markers', 'phenotypes', 'annotations', + 'columns', 'types', 'labels', 'regions', 'radii']): + return True + + if isinstance(value, str): + if ',' in value or '\n' in value: + return True + if value.strip().startswith('[') and value.strip().endswith(']'): + return True + + return False + +def normalize_to_list(value): + """Convert various input formats to a proper Python list""" + if value in (None, "", "All", ["All"], "all", ["all"]): + return ["All"] + + if isinstance(value, list): + return value + + if isinstance(value, str): + s = value.strip() + + if s.startswith('[') and s.endswith(']'): + try: + parsed = json.loads(s) + return parsed if isinstance(parsed, list) else [str(parsed)] + except: + pass + + if ',' in s: + return [item.strip() for item in s.split(',') if item.strip()] + + if '\n' in s: + return [item.strip() for item in s.split('\n') if item.strip()] + + return [s] if s else [] + + return [value] if value is not None else [] + +# ============================================================================ +# MAIN EXECUTION +# ============================================================================ + +# Load parameters +with open(params_path, 'r') as f: + params = json.load(f) + print(f"[Runner] Loaded {len(params)} parameters from {params_path}") + +# Step 1: De-sanitize Galaxy parameters +print("[Runner] Step 1: De-sanitizing Galaxy parameters") +params = _maybe_parse(params) + +# Step 2: Get outputs from params (injected by Galaxy) +print("[Runner] Step 2: Getting output structure") +outputs = determine_outputs_from_params(params) + +# Create output directories +for output_type, path in outputs.items(): + if output_type != 'analysis' and path: + os.makedirs(path, exist_ok=True) + print(f"[Runner] Created {output_type} directory: {path}") + +# Step 3: Normalize list parameters +print("[Runner] Step 3: Normalizing list parameters") +list_count = 0 +for key, value in list(params.items()): + if key != 'outputs' and should_normalize_as_list(key, value): + original = value + normalized = normalize_to_list(value) + if original != normalized: + params[key] = normalized + list_count += 1 + print(f"[Runner] Normalized {key}: {original} -> {normalized}") + +if list_count > 0: + print(f"[Runner] Normalized {list_count} list parameters") + +# Step 4: Inject output paths +print("[Runner] Step 4: Injecting output paths") +params_exec = inject_output_paths(params, outputs, template_name) + +# Save parameter files +with open('params.exec.json', 'w') as f: + json.dump(params_exec, f, indent=2) + +with open('config_used.json', 'w') as f: + params_display = {k: v for k, v in params.items() + if k != 'outputs' and + not any(x in k.lower() for x in ['output', 'save', 'path', 'dir', 'file', 'export'])} + json.dump(params_display, f, indent=2) + +print(f"[Runner] Saved params.exec.json ({len(params_exec)} parameters)") +print(f"[Runner] Saved config_used.json ({len(params_display)} display parameters)") + +# Step 5: Load template module from installed SPAC package +print(f"[Runner] Step 5: Loading template '{template_name}' from SPAC package") + +try: + # Import from spac.templates package + module_name = f'spac.templates.{template_name}_template' + mod = importlib.import_module(module_name) + print(f"[Runner] Successfully imported {module_name}") +except ImportError as e: + print(f"[Runner] ERROR: Could not import {module_name}: {e}") + print(f"[Runner] Available modules in spac.templates:") + try: + import spac.templates + import pkgutil + for importer, modname, ispkg in pkgutil.iter_modules(spac.templates.__path__): + print(f"[Runner] - {modname}") + except: + pass + sys.exit(1) + +if not hasattr(mod, 'run_from_json'): + print('[Runner] ERROR: Template missing run_from_json function') + sys.exit(2) + +# Step 6: Execute template +print("[Runner] Step 6: Executing template") +sig = inspect.signature(mod.run_from_json) +kwargs = {} + +if 'save_results' in sig.parameters: + kwargs['save_results'] = True + print("[Runner] Added save_results=True to kwargs") + +if 'show_plot' in sig.parameters: + kwargs['show_plot'] = False + print("[Runner] Added show_plot=False to kwargs") + +try: + print(f"[Runner] Calling run_from_json('params.exec.json', **{kwargs})") + result = mod.run_from_json('params.exec.json', **kwargs) + print(f"[Runner] Template completed successfully, returned {type(result).__name__}") +except Exception as e: + print(f"[Runner] ERROR in template execution: {e}") + print(f"[Runner] Error type: {type(e).__name__}") + traceback.print_exc() + sys.exit(1) + +# Handle any returned objects +handle_template_results(result, outputs, template_name) + +# Step 7: Verify outputs +print("[Runner] Step 7: Verifying outputs") +verify_outputs(outputs) + +print("[Runner] === Execution completed successfully ===") + +PYTHON_RUNNER + +EXIT_CODE=$? + +if [ $EXIT_CODE -ne 0 ]; then + echo "" + echo "=== Template execution failed with exit code $EXIT_CODE ===" + echo "" +fi + +exit $EXIT_CODE \ No newline at end of file diff --git a/galaxy_tools/spac_boxplot/spac_boxplot.xml b/galaxy_tools/spac_boxplot/spac_boxplot.xml new file mode 100644 index 00000000..27802ed9 --- /dev/null +++ b/galaxy_tools/spac_boxplot/spac_boxplot.xml @@ -0,0 +1,76 @@ + + Create a boxplot visualization of the features in the analysis dataset. + + + + nciccbr/spac:v1 + + + + + python3 + + + tool_stdout.txt && + + ## Run the universal wrapper + bash $__tool_directory__/run_spac_template.sh "$params_json" boxplot + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/galaxy_tools/test-data/setup_analysis.h5ad b/galaxy_tools/test-data/setup_analysis.h5ad new file mode 100644 index 0000000000000000000000000000000000000000..11cc7eec2383ce60cf588e2e5f7951b551c36e84 GIT binary patch literal 1552282 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%0AY{c<;x;l%uSZd)?`-tGOxqyux6 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%0DK7|zY&SU`HK_~`<+!_Nk&7Gf`kYzKbKeS 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

VH?gYpy#d7qJ zK2(qRq6cfo?eo<;OK84_KJ?+$mZ~`#hE(TxT>vHWzq5}<`9R70VjhiU0g$|N+mm2> z%ypYxP*ZaokC#2aMn5;B)0~qAepR2pf3J;(DE^tcn+h*MelLH6UR@>4Z?8ndjZG_D zdSx6)x4EMc`aNoEYPLtf$uakX-$c{^a)vKl=!u2>zOugewYv4Z`TkNTB9-hwL6^E?{e@vyCa z!SdzHoxtq(h?mp`kLHt84Itr_3Sae+I!13;R~`+EmhN~L>v;~Ub)H3R zeOSP(uj3wh)AjQ*J$hmPg5lhjvgq|t((~Kvq|*OuHv0AOXXVQp(ihFlVV>hnlzH@0 zawk1fQa06ldMF>J=wB?Yn=uaoH|O#y#Xddz$Bw=a_SeYg1iIuq3F_V8R+^q78hmP!{UaKbY#!6*DTden_>(gV%_q5jy-o5{oyx2M$zB+y?kpI#E0j=M8V�m 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 V|Cv4B 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 z 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 N-uZ{d-Mf6={{^MC_wN7z literal 0 HcmV?d00001 diff --git a/galaxy_tools/test-data/setup_analysis.pickle b/galaxy_tools/test-data/setup_analysis.pickle new file mode 100644 index 0000000000000000000000000000000000000000..2aa845abd9e82e74678fd5368b0ca5e91e0c7843 GIT binary patch literal 1366162 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 z zqxPi7JtTeseT#_aS|y43Z0syGIR%(4s7F1&x-K`N|8G1e0cRVj=;eg@Q*V8f>S(!= zCS0^F(0Hn-(~O-bn(3qNJ?Y%>E%L$Kkb^hIw^P%S!hLPq0B09Qe7iaex?MrC0~tI_ zq~eC7B3hi~S)X!*)gcPxSzezL&v>KNz;^R|DImYP`_^crVN!Px(JU_HU+Q>4Ohxk(L>%K-})M)Y=;4$kBg;QhJO#kNtGgV6!&r>CuX^z_ynZRCv?S$OyWQ z_)N+UFEe|}Wc6yvd#F&Cu%^Dp>aL6<{cMK>-c-N*AO z>$~VjvyCd6Sz-71J@|UD??D`k^&jyg@+u4P9L+ZUNdBv$jTw#~Z=gPiIBp3~&gw6^ zF){IUXwV2-+f+4m9rt0n zFAZBaIrhLb;GX@tGxwo?6ZuL!=OGzWS1Br0RC29pmv*ngCkr|j);&C@QwN`Z;Dd~R z4bNWBk;jTd&wuojF}}7PaI4Vg_g2$Qhu_YFYf+zze`~mN8tSaji7J_-rnN0!H2d9< zr>V)0TSUy^neL7F7vmIpRMa8W40nxRgZ@U)@vlmJ-5nL}o5Y=a0v=jir?K%geo;xj z({2UjZ?Ng!Zjq9rTnw5vMcnLKs0m$vI=^Vs2B88P@tRd3gZ(hic2?lYqXlDx=lCufYI;`w3%S`YOtEfwPyd1Bk}aG zij%e81D?xs@r8ds;Tvh`-DL^#_Y3#gV{8AS-rw--cytlscHSJ#UGQS)R^GZ6D9NQk zx8<`_RiyvmoxwN!oe)RHbsDlUT6pjh`s=7~e7zoiPEBHdzX_A_P>zf~thVaxutG`v zU;Ay}ke5V0zaHOvIBuVU{3PaE;Gl~8x9{Ko!_$h!^~DCx;A6d3>RN3_eZ1YayORz_ zmQ^1!_4}aDj!aOgE@|l5W#8Fpt(5dEXY+aErO>rDPJdGdJSg@>7Zqgv-&XxvOZ3|* zpKV)C^_sR*=(AKy0ON+Htw7JXfqC-#MJ8<6Fh zo)&O&7p}Yp>(KqwC_NMKHfOwc*miV=&LH>zfCojs9)5v-T~7BsbxqQ*4HJDz@YnP>Us4#(oi3Hq_X-yGm5u>PPy(qE259oB(8p|xY+}$Si~OWE`niIhlbhgkhUXW~`T`u^Ga&2H3T*{_-t4z-;A@@& z`jT0h0pi=_SUX$r`J!(21O4Qstl|!1G*s89(Dl&)sUG8dkw>|TetUx7vN+`3*PDW3 zXF=DLHQH=*CJlUn;A6T6{?BO*cde(Adh(jQZaPxl#`=%EIK(6GKc%#<~{@6$b>u(C=OgG&Gyb!cx$NhruBCC5 zmvO55hw~B-7bqh)VPm?qE|D~Yu7oxiTJRlrH5OUCplg^VVrxqsZCi_f1td)E~B89W^BSq;{Lj+M*5D;bZ^5%gy849DJ%U)t@a zoct~Sqy?YG`w2XiF30ekH}uitF7tvm$jNKHS!Jj->WbTQcl{r7GTOam^X89ox}$sL zXW0!I^WkN|SCVz8*SmHc%YWz#f15qs`xWuhs?jg&k`nkz1bt!@?qBfTyp>VxLBqZ+ zKQJArwzq=iKlpq^9B3z}F(o&1hv4^n>Mo0DjOV|2XWiC0(6wsQx;$Nj>k{~%mnI5I zdby<473k?x+gH_2Iw_}!bxnRM2O&-)7ACuB$r(TMLMG`z=)Y2KhPYgmv3$@U_wnxi zh)xyI=`z;2jaN#%An>dB`M^&znw-4y3HhS3%d9uMzvH^DrJ29mBcu4YD_TCU(^rK&s(f@95ZGn{BUMXpY1IM-|;!)>*}@&wx1H^ z^r!O0snT7@JLcPN#JrKwk$yT4WA9)e_np78CR9d;8kp)Y_>SjOIXBIB6!b`~?VB2( zRgig)jW$!_{M}h=#v`9@MJF~*qaBf$9RWrs@6%S*0JPx|7mhegg|CHst_d*rhkJD@7$l{p6jcJB5#uFwhX<+NO zTb**1lyrQ~`+Ve$gzN54y`j?{xD;Bj0QurS%cSHv0P-b?u?6o+jdvOxy>lf!HsCPCc6=SzOB=?(m7$ zk6TpoXTO{j>yO9y<{;kTD{NgTqk`ulS-W@2>81Ql;IfP8dj)+LI%VFgaiib1R4`s7 zGLHFlIwPL$6b*45D5rPFiU+I-jbk`86#DKOtxMa6G97GAypp~xdb@MmE$nZ>4>TA& zgiw!v%S1itZjGFhSA~t-xg4Jl8LcgzU%OKSPslyx;{DTnj`6yvBXg@3jlHier?dW< z_JQy|D}^^&3)z2poxL3gmW zjX2*$1)MnT>)FO%Wz2tw`WiT9!P3$5q00;Y%U~IeY;ii!-9S!@uGD8wh99)MdQzX% z@oIWpUS^O0zlL}}b#kVc!e{CLxLR*3r`>-p4D9WwX1Yrr$MU?plJR|5XT4(0z;=kQ z@Otl@bYtA-H^)T7Uvh@q_8?yf{ZjybPtYgYNcq%DMtk;6b#>jYB(QhA=Nc&}dc3*) ze(iQ|khpKRCh5@Er1dgNa7rwUWOCb;W(#@`<;=&uueMDrZ|jC);KIXYNjg&Xn=O-=Q1&1d+c& z{p_87Yl8m)IrACXV;?-_IS%lMAM@IYPyI&989)9HypAGjU(`+b4b!Ww zU2t8ZpJM@@_pyrZ)36`K{DOZ!yn0Vh!!~M4ZnJ)wt&`+yZ5mH{IXB9is8El$C+^fa zBBQwjHf(i=4_x#KbplRLy4-z@j*LQHZ`9U2mDX*6g064)*rzlfK917?+w9@P))HPm z3i|ZWb%80KPvO^qLWLPe3vRnwKICO|bL@yqveWQeX}vQ3YA2`GRo&O-k3d`>z1TSi zzP^w=tAPW-V+(!)qedJZzk4G-bGCx+6}_`xut78k=apqg|zpTHTHsVUP6|`7$f`=TJwp zIrLwe-m>GnB!B$?wq9DmMWSzD1bnfbtpctcmQlMUg9=}NQ8S%vnB=>PQqYDQXYLM# z?%y_ZZj%M@>k0kF@gS60ALc;?a8^ENZbAkGFIp5PnxNJUSa%&v|IMZU;r*8ROF@a=yAhn?y<*8f7s5q%NUR8%^5 zZ;&PUCDGRdoD-jQDswtQj7b7$1QejU{&;Dg5r;zO~0c8yo$RIw&guNlBk|Kd?c(q3a); z^uZH8zOz=KI%7x`O=YYUKX&Ci3lprY}|>x96Dtmd~-dl;hyb8t$~PHdavo z;6q)~@&4bQ%$ASD{DY|LJ&`lqg1jK+hpB2Ru)O1MZ3kWT%gJp`-pVE1E@QecaNPnw z-x(co{eRcnM`nJJQR6q2$9+f2m|l^B{Ps#_?W|DHm)@Utci4>l{OiDn!-beX5o}&b z;Dz>et$TpFCpTeuHUEgRt`K*lj(c8#LxUBtcc=4;&1r^g{N2r;P3p%yX zU!_8S73*~a_#m4t8UGb{?!VrSyeD+K-XFq~&ummN9F@gUBinVuz5{3E`I>zCaSwA- zX6^0tz{7~X&Tf(~;;@?j)9KK6{4>;v*1fh|(~*-wpJiRLu&))P$8H)u4Esc=pZ}ye zpjyfQXCtRho(XbC=njJ4NN*t1mjmIu7V=^b@Okk|hozfh-NsLBG~|7Esa};JFAF}l z;~Lg4hCtsHbg-?`oCkRBQoWH0ZxCm_Hoo*rIWHrx3D)m>)yv52_XU&ArfTLlS*~V& zZa?fJL3f$M@^0}ktV5gD=b!J1Cwi`KX$PLJ&%Nq##_(~Aew#{;t?vm9!}H+VMIY)m zCEFjz6f~;wh`?5xQ2!1!hafKL&+r9-PMP320r@S*@Yd8g>?6`yXwX~YdtU&L{tXNW zaD<*2Qj!|e2z}+)Js%gR0QW?5Q@CpyRks@dzy7-3{fmAvz**)#R^ly?r_&phJnf6;z;qV{tvh4jy<8XhD(t|`z`qKb#$&AL ze?Fw8Du##k+4^YRN4^>#G`~IYiHKX;V=h6!pPdyH=efG;qnk;TnitS}pNWd-a$9q4 z_{<{oJ9SzMydXSRA0yy1jHLxGhM&1#bWBVa^yfQc%)glPWS6>WlqY;0k#@cZw<_dJ z#|8co=Z?~p6zp%NC!Y(QNBF#V$j?vP{3-5$y7tLx*7jA%|8LJ`hX*JbKN`ldzW#xd z`GKM}?0NRb+)34pwvNDKTEfd$Nk@FPwMwgxqV(W9t-MF57(e%bqxIaNf>^|ZxDRCt z8h2`B#=WUL%`5!iHn~4fCq8WH?9(1Rxlx~%9WF?D=&h1!>kjGv&4vED_v{QE7a8?< zG-vE)do_E$=mSJvO2$e01W(;FOIkU&#Zg1+oGIIZk3_z39!JyM@9I3uP*HjR(>pxj zpAmWIB`)CO|bqPI}vk6!W;+kK%LKFhxY?fhxrJ{m+)oVR_L_owHI~1Ppy1^ zGUm00KkWZ~F8I8u{()1m&u6_dT-#y_{E+~Ng?%xHAj~@ge~5V<`={f#@&8WURgj%S zX>!>O1vOsMdr`T!v=3LSS>H8X(l@}@it{n<99>s$jL}EkD!irm@TwZDOR(7623!m-O-PI`+Fn&%@5>-n~#n;@!(^^7Ro=D$I+_(XTtlUuN3QBHyQa2 zyk^_w4t$ErZd=x%KNWd(_+Ui6%?vu`Q^$7MYZcTd+;_R|6Uk@Q5&Ec2%vUSaxw>;9 z7q+xgP^s(us~vi9)I8xw7d3FcI6o8#T~Fwnp>v5mC+hx-az~TO)*RyvPNQ!MnCSnp(~pez2Yx>IV3tC)R`By!aSF?|SWR@^!*0DxGKOum*FE zLO;B4j*5is8Hao(eAPmH2tUr9$*y!t75vlRS;$XkHdSl7PGs$pgD-=+tiZ4Z4icvb(7Pgh}H;oqn(H-O{B__{17P59eC z3E<~%l>M|F3m!%MeCRP^{jB4dAEy*?cxQ*F>nx65oxU}GFY16W|23yO!_^xf02l9G zzP6D`3>_)%+UAX!g1j)0k{=^oC+bCFyS1)HW@>V@`SRlj^1V2(fP5wDX+D?_)SB98 zD}4Lf^XeP^#+;UsCUkOliX_uN37JnEz`OZbcdS7N_N?j7!JQfW9R9DSE=c;#~ZCM^hE!8Kaa;7rPU~@DTcO zQ4iUGd@A^p5x?U5W3FvN-i2e)z`Mo^<5OhFKZP%9Ha|q1s_M>e&ATdR&jY;V@tE2} zLBNRz+-w~k!Jml!t@9im_}#tvxoNSqq1l8lZo6Yy-E0`o^5<=i#wDyC(|H)?D|AvP zUV^SH@O{lb{z0dV@K9Q-Yu3KI>E3 z0Uwnz@UrF1C-C#n?>r&474lWbZkg`N-db zJ_UcX829iS1n8N*e1Bi+Gr`MMYR>ngt~eN{F$i7=O`#G_~MJ2V;s}@*D;^S zS2xLrcN#jqFz2S;IA(sco(zOQJ>*6mG1uUmZE8E~ z_Hs2xor^}i-jyQNtsu;M3c3UOque6_i_c))a{pQWjQoN*Dq)`dDMx=^514F zrr=-TyUu@FTwCX$pzJLX3UeC;{b{?n?0ic(y|usM6tfNf(##8n3sBb=_jwuBKp*q} z`}S^&ZOW0KcVWxE?y*c?$i_T`FeeP%r?&0)F)s z-?Q<$g7Gr_rFeS?edt5$mtijy%)dVxeU5i-R>L#MgMyC|xa8feIercM$93 z{L+)guNIUmS>KlhUh3JqSFzA7#rZe*S42GuJmRt(`-ug&loVZbcKPHyo|-lL(D={x zIEr4^u}9H+@LWP%9fA)@@TJM1`%Z7W&lLQtI46VtVS?M}-Su|RLBq=pyvpL}=l(0v zd*Q37^2t`%pnrOuvreWbj#OG9(tv`p?RT#vw$!OuI{ZO`f<$oHbplH+Jy7hnAqsB2;#CFn{! zIyF+8;{AlV#2o1SKk{;N!1IcJ4D{_a$-Wj%Q<*QM7I{td*V{nH7tY~;55^n@=B4v_ z+K>g=IESNxtNE?ckaxW9AAG%|RTP`ILtejOWp}+xh~(=wExjA^7!rVa}~$*}U8HpeG&7__*^2$MSJsmLKE5yNP)vh^L^w-ZB304~qE* z_(|l^fP2Mx@+p$Osemt4STEGsrCq<~4jm==aMw$`XE7(8kHWDz8q9sIHdHzEMc-2s zW4h4d~~>)B9b<-!rPQZZOiFN`=lY%(>fvUp4Ok zF>XaPo0A4Fwm7)K!3L-&;yjT?L&kq@TKqx0h;^$8d|v^Y)F;;^9Mv2CZei~83i?KG z!-b7|vHo9eEvFyh7Z&tA7%Rp_oCVO9Mmil*`0A5*_{^o340 zeK%n4QH&e-?pyq7?owo|WPYpcIEu=zO8AwcW`4$Hz_-80wG2Rh7jdY8G}k^K^PgGu z52FTfBv0My@DY03$w8N5-_49+eJtvP$afD`(ts-q6bV>g;q#_9lTlrImTAp8HN*R^ z@F~B$Iz1RVyk*yvm!-GknC|fn^WFlFSO8y);Nu2<_;S)=$`s30tZsluiTPFF*4Kh( zerxP6u|GuKH5goc^5k?ioia&k8Vy`6`bywC%FI0R^^KL(7novR0tOp{dhjbA?M|#) z6iX8xwVWLFN5b77DVMBf|g&#A3jEoOip{8!t) z=P>B_6Fxn8-@KKC19ZTr3Fl)xiW8qtaWd8w>(DMy`u7-(_;X|UEX6!@UGj~NP>@YU zKf6}T;cN0q>Nds+{+QaU4sEawq7F9?_)yR}f#=1!Aoy3cgcp2j;=Vy06VLm&ret{! z{^9;D4tAPog!xe6d<5u8>E9a%TbCl<1Rn|bu0CsmxEGC-^rwHTwr#h-Um~2dQ-ggW z_$A=;7T!MY9&qwBj;8vn>F1tLCAOV`*95;U^6a-YM%uaHxx_i*3JLeh)s$bqSo3)! z&KDB;LFkRbKA-3&@y_=Yl=%F~*I(9B9|^uc|HD|!_(}DC3g&J2-tAVPzZT~aegl^* zoAAAB3H0G<4cyZ3{DkY$lJIRD^uhX_zDL~A?~8K*X}}-CxeZ&Tz6f}3Q?t2U=lqiL z$y6yWv0sEfY2i@hd%-`6xc|gk_xa?gV15MXcp`6%_Z4}zM^Zlqe3#vz!n(?TPpl*AMwHbA4r}FYq@G z&p6|wkLN7TL1F$!)SZy;OV<7PUg8dXu`(_2s*#G^x^Ijei04!=>hFO~sH-9_=Fy)D z_4OoAXA1jFd0PknnBd3Vi#a607y3@g?jLdfAnX04c@=V|SAIml)aICP1)U5QisSCNkNTsy0ne~jnId4I2^vZAN3OF!vzLUtwNi$j|0qQu<-8U&v=M9NWJ*H%ru4R>S`y%zGb&-}m^e z{N}x+=NkxLr@&()9>w>62mhk5PwruEPw?*o@4Rewq=9_~`YxgV^uc|5ui4~zO2z#1 zs59dE3nlPl3;Wwz!RnPU@RTsWTgUDv9(dY*Oo9K_t2`~z{j#pfG}IFu)6&#{Bh!8% z!NIRkKLuR??{nN^W9#Q8D)23}ehH)C9~68c(355=UYgsshi)&-uWd)Y>DpjunmN`l zX$fQfOW|zKXm^(r)Inr`sm(%ik)+iKScb5`LT)1rq{fJZtyB~`{UQ}v6(fJrP?Q8 z9#J^wAzMkE+9j_y2fhtCp#QE*6m((1=LMdi(~|uaBlMIs1;&WGW7K3AN{;*zfev66s04Nu7_V) zn2!rrkj|lu@h0Da$LF=%^?ibL-Vu1nRQfp3unR}O%9oejM;#K+HA21G)a=;p)8O-l z{<;=71kXEn{Ku8MV&N+d+Ao{52t0|P8%C(h2`4}aAebL=v!#ri4OCps^qo5LUe zS+2gJgS;fxfh}_8!-Xy=&d;O%ih3>jFmauK;k*yw{D%=r)^GjBoK*M5>z*LrxsOjv zrc;v7*$?=l#eCh8V$?%n|Kd7D|MVa=+qXJOHaCd%R%AMF`JJSu?(f&;@Aa0>bwHme zp2Kz*yoXTF4d53t9O(bWLPj~GSFJti4IkcrqrxA!qkj|5k;;Xy+s*g)X1#P|6i)qzpKw_9s` zlAAzx7UsypkWaBO6Eh|M))F->-2XMM`)L(@{WeEgxfD83^|Z+s{!_8@s(|msIh!mQ zx!U`-IBtVD7xFjy716&f!yKwG$C@Vf{cb$tquQv*ywA!tb>O#}emS(R6Zj_aIf4fj z&m+R`_3l*KyAr(ft*fW5-BBVhhV(3(iab&NHmrkwQ_P#|Mwj_Iq7Pj<(Nm{uB4rE< zcB`|%9M&ZH`-NUS^MfJZw9J@RtA{)x)EQ&o50S@T0^F_To6$Fn=>`*n)O6_K3EQHx z(mc&G-~i#=rCrio9&m-|U#wG*rgpFFH2&RU|3!z$JLtD(rH2Fpw}dKY$}DkCLj$wP zS$Dywi28JMX}#xQzNFqR_7Bdd5b@(1C7Zj=Lmt~)^06LuSj5j^Qr}vS{k7FqXL%0r z+f+E};YSs9%~H(o3w3cTp#D=!*Ng+Jo3{{%lPu;wFhTiLuQS{4j7jWNBt3`*3G*n^zy`9GaDgMUuH2vY~rQ?A!&->_h48#43 z=hPstiM-@`oNpnVR~Qse;V0A|Iq)sXb_R=z@w~nD#&>#sU&4*JKCP=Ath~XKioT9( zlJES8#5WJYd4}RS2heekri{`)0sNF%IDel9bhk9!HXm>E>^?g~m+aOdp%nMMvE_mpJ!H@Mr02Xv%-H zzn(+?{Wqb$_9pnBm8XJ^>bHY#Bg|Iv`$=hI0E-2;*D~|ufPK&LoXXJ^#i0-cFsbn zk`7MwPSS8H%B&dhu$+fqEc{#OzD_FU=f97+f@XtaPXedBxqWN$mjjSi4w?vpqwshn9eD96+jw}iSWjv_@OxP@ z{oS^x*Tc&f9fHxA(?HR!{|G+fpl@|BXonK-7 z2W55ptEC~mlgk^|BX4y(d`>44`D(Spxzne?!-~8%_P_Y^RhYjNUN}cBy?=mJ#%XC? zQMZm>kE*iRF3raPUy5BoR(?hx_gy8 z@r=j56F$HAD0nvEKAOM>CGaZ=h`ZQZ+aIAmiFFL?{7^f@qbqbualXSDd`jygw^oCA z%IxfEdjx!mc<$|P>3qahYWixn&wSA)o-XWK^u9|G=F-JEOrGL?PTM`Lp@Q`Z;B!S> zXr^TRRUrpQ1AKUkb&bT-^{=)AqI3I0ZYrhvCHB_#Z z7khOX_yXblTHvJXJ4Rp1C{&PMgI}%hPEDY-CvJZ@cN6(j;4_^Ohr;=^!0FNsB5B*cMCr1m+dyzg4fdxdT^yX=904k+i%;eSiY{1`js8h`~~`o?l%s6 z9B>c%@xI4pNx(f~zDHguxW7<&1@%zGWgT%osBnJka>VHZ^Y^o?uy01%=Jy6psCv-E z^^G3pymXEZ4}t!4Ch1|}sqQM)SAKy1O7N`*BYuBGBtFIa8@-?SClPsCh(Efjq3Iqw zckDpE6#aBFr1J;UI68YMebCFc(!Aq9@cS`GW^XnJPbm0dp})VY>`{N<81Uubb2+Q32j|{*X^b-KG6IPQ&~Y`;cEw zx&-x!l+FwJvzqK|o|KOU9*7bSPMV88>SEWo_Q=~}9srJC@N#00)+WGjYqWivATQ3l z|90H>n>aUGz)wfex4;0?3_O|WU%i3;*QwEox5!iCJgh7Hccb?-`ca^v`Fk%lF-IO2 z)>G>^{5K=d4WO>bBf>e-PgG2|%E9@7g8r7Jp%AWQMiugf`1_eEns8?Ixcy<^0gsoB zN_qIEY;_cUs`|Pff>MZiCaNhG?;P@`~M@^?OzAv>D^-E}+ zGtpN*JvHtD__p5jn;s_Y!drNjo-@zmyba+z;hvb6g?3f| z-ru?FvlngQ6A zW(qxc+q0~jxE{@bpnpx@a_sy;@Pp#HspvCB9sW0T$)=kd#Db@2ywT@*bNF6GKi(_& z;)L@O5kKzv8&dNzzv*zrDZ?WnF^WYrt#F+jMaT`15_EW-Zza+{4Z- z;D}~L|GM5E^P%2d?LR|@`>%QV8g10av=x=#hCW6-2zve!84cL+KE2~Rj`fSpfTskV zR}1++uleYonKIg3YI#{d5P4tdYYw2!7*3uvDUI_7+Z&fN1pnJ8@3H|6v8<%BPG8zeBLch3bs6e|Ja<`Ue=3* z9=2tk-l0^99|CV7)uK5fgTZqU#u|& z-uCwXYV~*xP3uuu^71kKx}*L$<%DC7yUNXfz>8Qmw}Ci}FS>m8$|ULh60FauLZcqq zdzAE8yU#W4W&g+1b;s4*_WzWfs0bOEg(4{-DwmZcO0vodNk*llU1v1TIqgJJltj0^ z_ey1Cl})0okS&U=-}@TR@BZg`-SY7~qdo+3Ya^!;{=({fmZk}~%X%q<< zHDUJ0Ib6q5W6kf^tbyK(;vRAEZ>4(L%vg?J-R1X<-$=}fXIbujR4?WJ%MPMG z#QJ3C2I{8ABhJ>4+8B=i{KV%wx-agYyOjTXeJr_ps`O-%6?CE0cQqaJ3+Eog{jtuN z&d?6)ZSk&V?l@=Ie5o;x&kgYT8E+KlC&K}O*Lgq6e;2C25)!+ zo*?TJfZI@CL5a98ah^OMnv~bSx9D5&b6H;;3B0i5sB-VaIB!Qh>u}}_`b>InDq_i< zmz|?h-G%>O0s4Twmo2tTM4#h7G_M`@8=KF6K|OMQu<~gizU~TfezShn5O}m~|F~*f zeBLonJz6b@=DuS6@ZSeq4?BZ7EUVXA#PYrhb2nB`;rTc7JipF;ZZ!AfE5dWD4DZvr z5_l`EXL=~Peirv|*DtLitINR0rh2~qsF$ff27W&C2SeSJXgKZez7+7YJJ`C%%@=*_ zO`Mx_PB#}inEiLXHE^!6zM&)PSUSi0lt^Tkj|X+{Ddp!N>Oh9WpzdV#whr(WTKD05 zpPJpGKIx~5zt2OsUuhj52>maM9cq9Ebp9hENw;9m{@{xeD` zN!^$mdp<(c&FIqyUa&E?!nwoF%K^aYsZR9@a60OP-W|TAn=WPPILXMZo#n6mSHpkA z^3>BImT{#0h(77dQdGQ;#(N2?c((r$`dP+P1CGah7;s2e(HF6G zv>5v|T&+Rph?w)CK4QMnI44&vsNZ@Jb$QiO%Qu>C@TJ?^vr|3Jjb%~4)+LHXVa=PHS4s==OH-ZGT{EYO+-Lv_<2* zTX<}G$Zz;!pETaItTpDZ1&j1=O;qxF1V3j(md1j)o(eM9Bl1ME@$iA8{JU(?KLC$S zkWaomw-I0DQPYPf>eM@!(FNqJ|1G}(NMywE8`PJg9t{kJ-rs8=T$O&NGe>4_dD`b@+bbVK>zv6YS(Bl)IV&W z65x%L_X1umn^WPu|9j1C|NKefIlhi1Egu#C_iDd_d`{_Hw-e`If1BX6lNGW2TzMqU zpHrASGyNQV7QOc54c(#S>)R1H;kFNTb%$kS-o{aH*MSGabWeN0^LSCX`{H^P_hr?_ zeMf!pF%NLM+;U0~&P8@lTvYJ>8RskOb8>`_PZIEX>c1A9K$gtUyVth_^W$!AOGZI= z^yRwVr!VypeAe8#9QR||8+u)XWu@ZXuP5cxSN)|ajg$W?H? zL3<_dKMsO7RiNMSP8Q958Spun{t4&fJzqbI`W8xJf9quG2;dl!E_>&6N8iHqVy@8d zXj$o8*$jUa>SK>OpYeo&54_NQzvO5-_%t+sWB~ZAzZV)mSRcpz@b>~o@!0n(5q%Er zH}1vbK7Y7(|Ni5G*W(~~NEc9lP=E9yC7)|Z;hP)vs-aRBbsgn9*TNsT^Zt-iO~(X}hF zZ>b+$6y_q2w|ZRgz;nNLFZmDFDXYuiM_Jl@P*kZZhR<c9R{_&aSy?!x%bOMV;RUZXzRy%NZfAGMj|X9+(@)Q`s=%~tNjpJ)5_ z6#B};pPk#iiX)Jx*VeYiex&@G=cu=-Z!!^lM=$ug(L5)dC#-Kgq~vyNMU> z7DE3T6+3)|Gy44|IVB0NpethYZ}7_4e8@|DZ~QysiB`Zrmd*`up6o7(J<;cW7|(|p z#PK^l++&Pqc|*(zVvuWNwQyGPSm2%Zqs#rC!cY5JtEjB8&|CbyXKTMy z$iC3}Xe=oVNbhgf3VA%Y=Bj6*uDo_Exci2lQDnEe+k(>UNU}*QQD%a^k?EYUUcy(` zJupQ5z~=5X;1MxDWBAyGN9;8L&(NgohW)+<;1FG|>y_Mvj+EjsxJOwYQ%^CUXe}ob zA8eQ$-6@VZ-)!!59Os6w*Mbnz8uLZ!8`D|E`2fIk9z@U4+5#M#`8nf#GF~HitjrHd z3Eq;O^xHO^`x>jx#4MPg;`3_EH<{1#C#-udN1vCd_s+d_Oew(gFz;29cJ;W5s5Y)r z@8=AC|E>C;Kfgeib^4g@QBC|lngbOnasn)X4^ZE)UeJqEesU>rCF)mxL*(D+t4P}P z>Q*jZ!Vld**=%>d%SviQKsZ@f(tdQHq< z8J`jN9pgik#F7h5OE*+&q7Qm|_VtDn!k6ko4A2zK;1>cUTUD=>K@t>vVK&=)UNPJ zXMQC3{w()mGIS?&F1-YPhO`eq&+DRv?FA1F{RHF7t^t0w)3U!F@~fE6F+76LEx;3E zxGm1DC(VymEq53C-ez&6_HCcfEgYlRzE=AiNk+|f+iHRR#MX%s@I%T|x{ErqQZFFAPAEcYcij@WoxuZ&nB^z@kTF33Dy8#*|Y9C2E?tB;QG3B+7gGcSLw;n)N* zcYvQD%RRyQN$WmM_>t3`EZpaehYehv^*>m5j5qWcKH!724rz8pU3#;R<+vpAeZcQI z=GX4WbD4PG&?T_EGt}{{FBm6yP0vLgIfu`sci{eJe$EyGr@;Nkco(*qb5p;GUU59f z40C6uC-vt2ss#In`SHBPI(oC>%S-Hch9h`{5$5}X=gIn%CfL6;cL9Cs64!HW!ob7% zwe`gjDc&>lCzt@7#os^B4s`&_>zX0@qmM!_BoUvtFL>VO0a8t@bCwfP0-X)*gK(d- zJR0zC84n5fe3sp$zed={^n6yU;q_|@@D+;p?L)mi(eKJ`d))WTM{cmty%7F;B)E@Q z9W?~J8R}<#5cPv&ORr};;ipUeAJDfl-?-(duP7d%tspzgRVOLmn60NQ%+D6bhTq4z z#`2oRU@q=#vf=kw;KvALShgEJZyWCh&buS}{C&ugn|hdNx}x5!OFm~ML!E52!Qp0C zCDFQ?JpDX)#PQ$WYwQB=ndQBVRg(G_WR5z{7nW0oIRfK7;ruNcW+U%~{(<$+{!-#o z+N?;Kf?Sa!XZxjKU9dT{0_OzH1yF&1F3U3*t|=k?d;Q7!je9HgTHj=Ad@qLoJ`;5* z`m=F!el#{h2gh_p`2H++B2Y>et{U=E>!iqCG!^}*FxOS#v7u^ z`j$T@-0Up)TIgrld2t24a^3novZ$7jq?#*Noqh;BQyp`2S|CR^YcQlUKDSd9sM-VON5SpiH&ws@Nro`CI`Q_ z`w=Z!mV#`{I{fYR5joF$F$JH(+*J|#yR-a()=V#=;Dqx zS9{nM-3^~2#$@VP@@X1cJ z8+$bOHe^nUYPD0(y<_B@ec^G7~Y2*XoiX-bhD$xhy7+g_hCg=S7H*${K zACi%7WnRPvf8S}CYFak<&WwN28F<0{E!%vD#gKqg1C5L##r)-T4EI}aBPY36OIjx2 z-eY}~A?{a-qoY1z_3@l&BJXFbFsO>*x-isH%s2O?lI!`eA-{m;U!8y-t5aD=lj-ow z32FCi(g1;@it}JS`W}YQ;#}+Uan*P!?)8hStM5I(6wUoBTcZA^Izyb>exJI0Yc>fy zQ0?*8AESSJ(tYs=1^TVC+sCYJKLGf{f#QuNw?toTBj#`G@V(p4c>SO)d>~I-j~(kK z@?B7mfTll7J0q4@9dFfO4BUaP*8}UJ7omPpKEf{o_XP8W0RN1g-{^l?Zs<;;D@j|XQYDn@ypRSQvdl4a()lb zhab_Q&)>fr%7}x*y<01BFAj-zS-yQHbUQRpG933pdE@p4|2q%yTv&e6V8QeLfPF{j zeRz)l%hxD&ett6r_376Bfj;~7gs;?Vkvl`s57T)k`g4YBp2a=M=B^?qa3SWsw9ik4 z9uPp#CGdGz--dpIJ}1cn;DcAPyO-e}(`fzu+pR^I&rm-CbNFTs+q~~T>?elnfOi-& z$we^?=QhI`*J0ik=38WOG=}>JbP)b+tAKZw!hdoB_$;5MzUXr&l=pk6kGYOMj=Z_O zWQB_n_#f0iuO2!->c1cteoU2e?jL$noOf4ZNL|M2eb&G&8NS~K^R9!p(aG%>_QVF+(f-64o>8GfmJ>Q{G!<3^9x~ z{aSedd=sjlzPGr~t3xqQ|N1GZ6Xv>1{~C_Eiq4&ef-nBH zR3%jh&PV;2@SGG)H@kU#0)K|)iQ>F;s_79Cfcq`^pZXZ>rqDT1KNa*dERPxY6dg~5 zhI3u~7jxn3@E_`1>LYw8nb*l0{JE>)9%Sb}_Ge5di{PXYnB$&J@qV}z?``mfomJ>R zm=6)=pq&F6AhH$GgE%xSIkqGe*x+>?V#_qfzh1D_!Is7&m|WL@Rd_`epkBY zfrm)-1YhJlH?@`E72=%x`{L$?LEzul4?kqB1YYsRCb}TDo2aWWufE%2_&LpFKF5@p zpuci#xBoMK9=k`1Vz|HOboik7yVUhOj{BSPY98R;UcNfq0(gSiyxWUL4j20Y=Pa8) z_ZNKf5EbXIXDj%8I}~}!GzTDE%Ab=x-j~*fUq1MIY#;0eo}GWnaOylUPZ%lq6&Eq* zqPS~U!T$iC>A96!+|lju2|2vqx^r*}_l?APM*E8lPw_s1x3avih8XTUg!|>dlr;tS z@aJeBQ{#yKY0R3vhrmmyH%_cOv=BI=<;)g=UGUr}Z&MrR66MeS!tcNM>3EBJ;a6ae z`qKRJ*ZG5xC()?oQxANStvB3*tp92w`0<-?4pZG_cg%05nuhl=7V821FVp}2jV0GE zS><+pjJox)wxUf7%w39`P3VulK{r*LquVhkoyX4V0)E~1N9xcM>dfROLps^L0=Y4q7 zR~zRj!}&*XUMbNQ{#xkIm|g<-Im_k6J;>^J;EYVSmyS8H$=<*>@zB$})NmYm1G#JJ zUry~CCHUDx1s}p2zEl)vy9%7fA#UT`vzU7o40Qd^8Q+uo;{*!7qg{gcgYOY*Z=LKs zLBV|~6sWgo9^N4AGwLIBR`~UUkIZsp?})j=WP$Uy!~2ZT3M%;m{v^%od@Axm;aAD( z8Su}Fvn+$F&`&Ubdvox9j;lSAw846&Io7~47*7`UAA7E|;oD2^Yt)sD=deQLT)M>Y zxsH+0?V|2wI5hkNxt6jg3m_tRd)+N`5u^;(wt4q34GUI zpD70(PS0yKd>*D7c#rub4LIH^-d_mjoYX%Xcss*gF%Qx2dg{^O--5r954@`|JNbe# zlJlN~9^bDR^%B*4mWlb@Sl|S45e55VFo!VD4qpRYlKo!Xt85N~`twxnWXWy=)Vs!x z(Y=Rae$%Oo?T?g5j_b4p|Mg*7<3xN8R`2w~oaCp`*EztASl>8HRcjnhm_JK1UN0rX>pM8&xQKzzjX7a z?>52@(gVKa)!>)V-0>a?K3^Szee-GM&_LiLjOU{ZUkF+s_+$Q@U(hFPhv3J*M4lt{ zy)F~`8TvcMZ@G+llkcg*!u~?vis#96RzKuq*OZ3!>PO?q9j6heV=hC7d3(wtZEe)W zG=Fq4ex8nZq24?21#dP?e}VnNeEllH-=ThGn5Q!Q3FjK)PhdSV-vQLOOdpFrsN>~I z?Y1Yydg_etN9VZcZVLl-^@7TONQ=Idk@jPi>Aoks^wm!%8*Ad_t|I2yqWV-u+)kC4*hyGxozpcq= zEBND4UTrw=K$?HiOT5o3@Zq68f-|9arv6GZ#9aJPG+{nR*cWkAS`8{s!}>Fy)qFeV zo>YgSdl=_sm%&f^qOXg1cp>N+{LtvVk^fo7@z5QDXR{jm$$n+cM{b6Gr+MA1jY=6Y zu$N1wsmpj??RXhknYg!271p!g_=Y`QtySFTYXa^sy6yr+?n1LT^6zlX<>U-0=eyv1 zXZ_b#CEp*$m>a~~ESLQR4?_F?(QZo=Jojrm`Wt$m0Vkt6GY{atIyW->2h5W3_tskE zoZl9?xoN0R3QD@PHxzpM#AwnnDQv1CevTUcDTL05@n@#tJ&y9MXrGC5f$;``gI*mz zY#5#gt9x<3us{D7`-1u=z>lwQU_te(aQI+Te|bwe*NsZycSiBDtuprK)$l$x8Lb>W z2mK|}XDt+S&=Jvu@wm``FrK3W^!sHR-YGS}TWQV&aA<~GYKVKG1Nt$V->?e#YvYO< zo}#W{{(7n4h0%Q2{eowR`i9}9=)W|QrWVw||DEwK$I3a*QV8FuO(Z?};>2ctQ ziwBzO7a;un#op=cTd9>d}Kg9ZE z;E;>!t{Y|H{HM=NZ2->6BQ7n6qON*U`*iR-E$ArC!XiIdK_5YRwm9QhF3lwHB-g%b zTfZWP%z)rO_9A$cUAH#oxdImrI5V$S2lp-IC8BN`lI0$L5%nkI|L6#v-ZQa(UkV-3 zKKR-_wXZ$568iXmjpn+V@NK3#iVHFl8NGYTD&Rg0=g|<)rxyOK6mLL1-K+7R?A#hD z_b1pQBhhsBRPe51Wfj5VHNVlJYV$l@bB{oemtra`-1sq;PYAC@7b?kve378!1+pf zB`<`Ir5(;?I^PA(&F0=+fK#t}{?bPmz7f=qs~q!d>c96ho-~<#XZI}dPM9xPXW*Y^ z6O%ukLLM*0@h;&0{WT{p66ZJbOTfLve8Mim&z9;UoiLxLzR;LkPM~>Qli~CCq-;R2 zK6LD>a|cQ{!;e4BX=n-Rq}+rr9v$JE!g9~Q!=IM&$XCM``R%IS-Z<}>P6@nJHvaW zJJVZ?isAhP?s4X?nke>HfsELhmE`ue7I={z`uXQIt}gbdx9D?1{nw`Adp8&Go$2y7 zGZsEbkD~?8415HNyRMd!QIcfMVDzgj$KC`yO_QJlRf`m4@us))8*mP+YJ15xZlK7G zZX)J9;0ZC`#4pf+QJxRZNp`;sh#`+Hd+oi2eO#%U-J?sLs6$X6v3`BF$fwd1JPY94 z_x82&J_}z+#shq(AU$_)zpCQ^{qU`{%P*9`J9Yl%hO`&`eSeV`{04pN?3Nz+IL}$m zoUelOW`W0>WtPn!i}RSxhZ^Lh^z_i=y&KVo7mal>13o*(zt@Cu+0c#Axv3HKCqp-y zC_-=_Oz&vydP3~0`*PxMuv0%GM)=`DugvNL;1{b;b#1%>-$SOGMBl7E`Q*V*y+q%Q z=fLpfW8g6iJthAEJdgQ>pZe>X z>Ndu!oX>s!j{<)-C|*3;eJ(yfy(elgM>J_s64Mw<1|Ntx-opX>wwAN9D&AxMLG!jT ze`k3Tcai(z|L?teBl6~G-nAXxtLq#8lik3Jqj*b$;1i(_Vf!Dtn6l?#mPS}d4F5TX zdXMt^QFk%jJI?n`Kb`+6o-K~K4t|?j3e~OaNxqt_;T!RM%g@nV(Coy_OQGW z^eGn#tXg4y!f>ku;LX?PR;$kv`abwQv3YATbVJmy8Fkmc6)K%RI42nI5p@L155Sy~ z^>LSk9-$5TPm_>X72Y$`1DVK4RM)eCAyx48q`CwLKL1*2sUnUb*bGzxm(`nb-o=mC zpT?T-hs)ERTdgZ6-EwQ~eA;6FQ(uv@;AK!BgoUV!A?wwL4xafLYbXenVHf~^DO>ns zP!)1e>L&)AH^n`|>Wu5aORsh9IN+M#wW8i;b4t8lhQmNtMf;7~^@8uZ96F@y-G+Mr zmt{OP>@&vKLZ8m+a3bU9#ZV(D)jplf3<>cg!((8e&}NMf#ls2e9llA z$8Y@Q#O|NoxI62yJ}AC@QN?{&?8WnHi8`1*7n~!^cd{$5N6b8BM9u8Y&CPQ7){OgO z+HE6pF&>;AJ>MvbuP@Y_Y(9WlJJK zRRr+l^JwT{=sloTjeMciPxC)o$hbZqybBtr6!W za_5-u#wzqF_ZOPT#)|seSe#Gr>tX$TCnaIJ1UKLz)ISOMP|m7~A*urKdd{IWzNjPgy4H&%+Ov3o)epAm~gyRZ7BE%;PMMmMoCHlf}Te)-HF z>}Wixy|i%oLd-K*zV8_s!KzYITPh>|KJ7ow=q%xReTy+~rv85D8`wU;eqr-|9r$fi zetl3Jsc)9q>M_o%w7Z*S8OLQr%~ZEvuzbVrefMXW)da-vN(7V^I5{*}zBI zu5$mBHc{|5qJgv0T)Q)qwan4b`L;F~Qr>~}_l9OfQ zOZrcrgn8ZV_tzGWlaX(pZ9n!o0zbX5Q>X51f)6eA)$jvuK>43*xbF4)eav_EObzJN zM&xC8k`emj)E0`q8+gUhxJ4QBaE`OOW~R`I?hrZ@@O{|()t7TTEEoEu>?HS$WEtnp z9+7ff2>Y1j#p8XmKJ=~dmB1W6HKU>-;x)&GA3Z}Ku{a>~jumnUs1L+c!Gi^!$NX}z zPL8@-*~?Kk_%}R__PPx{Y_aS3f<1C_R+Z59;SAu@kDSBek4VUgN8U$=V4kz}@#HRt z!Bfbb>Do2}>yP1Lp758UdCJH&08L|^t)W;4onrXD$b@hGpSBaGEtB*4X{z{p#+Z{( z{m)b0R}##t7#{E)b)Rgarz`q(_V^A9|0~BhetwRJPxF#(oh=>VzfW^9&@V6_ekpR? zXkL7Rgx9UbVmeG>ic zri*56dhq9@k;M^$6A>tH0jDHy~YC7Kr=7>3f&e6JMeaqC7m{cCQVOHorJd z)M>4t!=QNTM=7a?Jgo@);egbH8uKFPkCI%g9&H4EPJN|-zgdM^3 z0w1`e25Sa8V2=8EYUbKw=v(P}n~1)K>NYmOH)=&qh4hi&7v{zYJ`MO*^n1XUn&}%l z#qwMwBk<&CjyL*dhIc;`_bT4&WkrtL?32Q0cO7)&!?o(&aUU)0{dDC1hn#1TnH$IR zQBmiyxhe23_I^Gpd5$Ia=hj7In!kPkpAPCT3LLL11t8`|kT1(Sd@$S-MOF{k{MR4+ zY|CLWRSjoDxo+Sde69@TAA040Z}2nc_Y~CSj4zxaA?07KPjp=o&UNqY6G%*Px2d&@ zMBU}8BC&(w`Zddp;y6Yx=$)wFen}i(_fw?AxmG$M-5xoQzMCuV?vRtZmZkGXcMaqF zC<(YmM3BiZ@RGQmMVtqi51(*)m>db-4V#Z*pD?_kKh8rcl=v7<47NS9BBw*i{PNfK z0hr^`eUpplWB+$y_1i5lB7vCkO)4|TTAoizhJQLi%{2K@aQj$w^+F=|ZEN364T|4OTF02f$l?7iHeX(;E@ zj)~yUTRVaKht}Xcra2y%OK#Rb{P@FK%vGq4{g;e?j&&h?9?@FhlF+NJ(Ar+Fo0Lkb zoDws9mSdg-eDGN>;A|8x)ImPZ)h^De&}Afh_NZ6y>m}s%&4RaWcSLfZ(!J2N#%)S$ zJcV-}+sMjMK{`$zRxCR$C(&bU6AwI>aJ|Vv37;p=P!ZGXo4-xMJ!(=k#m@6e3emeP zofSV$Jm(fNjhZCQ>t_yF&?~?Is9)*#z#}D8C=NZQN zGzDZhm1-7>QYUU~P}731n${u&N_``x)PFW+Dmtc7E%TSf7E zG)hK#?i%@h(Ny6Nhq*W1Cu%qc(uRAsH-vv?f+rFz9pJ0+qhP--{AWy!QXeX9!bwD6 zW6k2vKE(Rptu%i#{`Y#~9A)!>zvA;x#r@MPtLh&15##yIN+4^yjdmS57Cs-8&v-bJ z>~+rnbrN}`AA3Cu`-Hxl`7RbkkU-7$zh{8o#dsy>1_6ELpgq77shckW8j#k zr&o9TBhIfi@Qe2_i;BU0&+4ie-iN7mP?4oCx|JtR3Mc!IB;0+s47neaH!ch1`)CAs zJ2YR$GM@WitMGF)-+umlE`&HHHyIjJ9z@clUk90EUc+*$eUNu|Rh979Q=y@73%HFyRzp9kkWt*;*WLdQ+>=%+#Nv)tJ1 z?HS;VPmgWSe-*-W(x3}rJb%2OooxpFUOrwz+GbksKLq_XeJ&|qWu(~bv}|L~1ajQo za^jcNFanz4p|lwhybi!Tf7!)Kw-x`>jY}fP$c~2QTF9Z9@hE6QIi5f3Z?;MJ`m_({ z`|fEp35agUa(BNV51>nxzu)voav`Yx^XI1Uv3&RY@pkxDb@#WZ z^t&VBy7g$GuUr8i2&%_*hdu^IJeyGWSQi|b<9t-qW2ke=GvpJl&IWIq0;?<0H!pIW zF{3b)%zXUcPHim-ktSNM%3X&X8}ir$q62 z1p2yr-s?gdB@%w0U_Fi>ferq-T z*IF`6=lwpH=y=j1@5zG)zKPuLu6Gpo7yAtz)U}K=v5Ig$@6neLnPtF-!fX|(X{mo@ zdR_?seZYU{e%4b;$o!{4UiQ&Jq@QKG)2)3&d7tt_-x^_AA6~%{}hO`dj{zdy_ zq^a5dRgZ7L@9B%@AL*7r5`J6R)Z$?jft$}ZzXzyi(Z`PZr{exIzrf#uUfH@^Fu(6` z|KYK!sew1X?AC^xi^fKh;i=zBrdCFA9aaBG?jO|?`L4@0EV%GS%5%xuOUQ!<3%c&W z_wox0J9U&i`S?R zC@;nieTLb!H##$*d&5742O{}g2|3JR4mXvX)zi2R2e=mFbzok~@)M_vyzTGeTpbzD zeJ32k(H|Wfd-i!0JD2n^w^)!qq4}1n5#+vMX1wWuNUj?|{lW6FG3R7B^mYlqA16sk zPxaZRufTtUjOu|^Ecm(^2WLG6es#2ESgGF2DAKX?(CO0>_;t_Q_Nux$e5VtAtxjA3 z-zz(={fJ&+T#qwa#q~0-5!`R1GLhq7z?H95u4te>k4#5H;<;})`rQ562fJ0a2_?>V zx`%E(9>UiX@F=Ed!8&*U-XZ;Lj*RDEJOCfR>GQyOP7&lqzS*ZQ=T?zA*MWT+vA*eZ zRO^hop5{~ALg%W8c9;m=;OJP3CB59k$eS6{?d^Yr@!VpZbHTTZJTK(?({mJ}`8+Yxx>dSfgcrQ%nzfQ{M z#K8Yqp33)d&R2YxD0H#+<#+DX%Bl>&d`d zfA^e2KD+Vs>to8-uOMwV{4%AvAe09`dnR%aqW2}8^^D^<9p-tF+U*Stj6%6SB^EhQ zbRNAdo}b6*D!wmkBz#?gua#nW_Ty0!9zOpB+&yUb9!Tjew zgdVk>Pk5)Pk^Fwyl|U+`BVYBp30;?3;moPX)bM!H{hUssjQbIc7P)`^&{aGN^j)8% z;`tH7fKSr=Vf4++NBDgpTd#8_NAoLnQAruPaD`>&+WCGQqc`ew_rlv9#ZvB;o#E#mRhrmYl%-E@R{Qy;v%IzyCsq zP4muU#QF3Hb&!i^K+mmGqE+F1KOMOIBi;Xw97*Tvt2|iLJu8JjpKeiF`$2@$z-xDPhP47?D$BAC!+E2)g+_fTO9Uq8_b za?fef>p>kAgxT>rLN}->>;3FEd~K*dl#h(`2sAD;2S0%AL-@=s+FWLMa3%QA2>jJ- zt|DKYeYe)&{QWaG^2<#0`}@)!E&mL@8}pIJ+?w&Y=A!Puee!)1VyfFVqdlpc$hZM6PlBTzN5q6nZLE+YsUIh;NFb)-$l%|Fppq<^@-45 zER|@Qo37^lO;n`#-V$m8l>cSS4b4c`?SLOIRiduPhSm1o7a&f?p+I$ zHRlu@_nRkj5#}jL*zG&1Hj9yy3E;`61Ll0-y`+6Zf7GXM(4Tzq`L`!?p57eb|7O9}Ur!MbHR zFyM6*u6R~_e1nbQ7P&cI1g+*j+2=tol%d0&NjFq@k^7rrdt6WMxEn+-lM-G84^pZ9imd=6eO z(|cY~@w(y#>WU`S7OVb*azDcna?(Bg%sVel;d9`Ec>whjUL++>?|%=Anu7UCgYW#` z;0Ll?^y`si$NS8yaYG{b@2h}6RQ!^m8Pk@LoY<2ehpW8#z5p)H_N`$M_eX^f3e#hU z$;kVnb~i6;2a{WOwydl@Kp0)|%b^m_gW;AzkPz?7CMc>K%O!C)~QXkDx z2k%7jIrqAFVmPM#nQZi#?M+8tOZp_``B?5sGH2&6_f>7AWM^LR`iqmmKc62k`p*rmFZ!siJ?aj#Pz3i>qWYZ{iybH~pM{upqvKH9G4h8C!M&U^!x0zPS1gQ^;~ zBX^nRLu4w6cha8Ui>9O!#pR*Ol&=Y7@vQtiFW(@iBB}b_0nDM8Klo|n%%i{Ys}eXO z`f0|08U~(2OT7!@bCO7T-k{AF9Wk%_X}%{SM@9TRU&c+q+_L1cm0YhjiMU1uSO(h) z|I0k^N2%U>BkH3~8)ob8OCv#gE|FvN!Gk^d)2;*jUIJ#oW?utu={@rd@fKe z?(shG^$~g44zp(5)dN4F_DHF9M7o4%z3|btNfLS0=nI(-VHc6hj{BbFx!(^Z(XJ%L z|Cfp^+1F;5O+y0jSDr=k=h+f|dH)@H`f*Y`G1_-duVkh06#-tzd?caQX7`4hoX_#! zfZs>+Y{1K8eV`(iDAYDgKG`>%Kfl6w&MQ@7UU*_w&4<~^94{+_UQ*He-UkJqSH)O6 zJIs^m`<%EWnoM!*v}nRK;R}^5^c6TyaUH1@!rzH~4ifN_R=P9t5}m`}u28 z2WH-Ha(Y!FpEIGpJ8TtjWdA1M9<**5j{L!$!`3Ya4p?*FZE7&|71foe>Qfe@KA^sS zsFNA5?{yfz2Le=_$9+!1a}K~qXj!N<*J~Hg^A)O;WKq7qj}PQf#>1AFns80YNuUY}W@pNM|BF9$iO3>V!Co++JEG$!!>KL)w=RL{Cf;FQ=eY_7T)KAbH* zO?FNY{*OU2a;)BV#*DM;xS#k+_(d9+zbZ~oA=^d|t^2k;m9&|zHsqsIFmRmppYwYn zSB%bkOYnTCo*6jd%r%z9!*55BrGGATor3#{@eANX;&G|No*{|JWWdPbx|@+d!=4A$ zA=5Jv?DsN{#f$6kxq2gq4fW>PR}-X@P*=>@w`1#>weVG>egmDPWZ(SS1lMkuk5S$$ z@D0XiZ4F+rt`3eWS?E^YcACq}cCNe_fNnf8Ra@uLJO0vj0??oWp(7 zJl05*Od4xJ7cf|q{g?l6P%BL%R_9t*0Ga!-t7v(nd8ur`DyqHtA ziD9Jbyupd&mV%cLo`%2Ete`XC!!uvzIVl`>I|MyV-p$H84{-l?f3GoG8+BOm^5-vZ zMw4%btzYa-Nn)R`davNUCkQ_3bMS2@9gxjA1s?>{ZGV zE7+Yunl9hfd;|e3h+}{a2?ah9kImPf7pzAmmfA3~qP^~Wb$#16>EJ8jZ>sRhdNzxdZ zjR$aOrdNN&`<;6yC8Yj~$q=_2a^Ck0Od|E`?nnRpBjxjEd>%znQT}x3ahab*OYr@v zZ|Gtr*U!vFj*DyP+{R%-pNgNu=GMR?8Gm{N^tH6VE5-Yze#OP$n^q5NRRZ49Hm{P7 zD=j5FH^y1$1c&2%39T!fi$0m@P^Z9;FyYLe0T0&k?+3mh^Ih7Z;&UW-IT`iQL3;YQ zlJcSEWMe9J8TM4yMz+&57Uz5C!>%XObelY2IYPHu|lb8!8HwcNk+9`d*TjLZ3k zevHw*_VSnk~n_@bqDk9KcnRR7jTG}gw$c5`{L_jw{^#)TCxX;|HXmT{-QQRf);7@#clI@Jo-)+ME zE>|BXbH4d>!OH}0%ya|ANt_o6oN~p5nxLbo$2t$b{^P?^71=N`Elh7uIH_u;7BR** zf#()`V7>L<>XZy#!6MJ0U+4Vd_sYFAtoNL>o9;t7exH&gBSmg2o)+wpkZxAp3e+*z z8~wU8<>qu1*U7;b_m<_8-v>}vGW_8xe1bA&JnVJ?ek0U>tgVbx^jw#aumpZmv_Aum z#{AL0A;*s9h63M9m)KlWK-b9h(cs}2oQzuAg!0E|zl8arCJgYjH%0Mt^gtBPw*cSG zW!aGfw@2X~qrS`;z~Lyq{}_I|lxI@{UgONeH|^&okq50`_Gyp4Zfb1i%0uvfVZNH+ zLo$AgZUQ;~Z$fOCC+eT*iy1lKonD^g@wB}>EFQx>yi3s{!kH@2Nl0MWTkN) zeW!S?N0o$fz0qv=dQg7pSSe{+)qRaCcp^-{m!Tw%0E9R04dJ-{3I)j;UgzxBUBUT` zd!@wMO!D~x_$?>A9D_czi{!jL@B^n9oygm_5V;(m2i7E@&X#%&@wz@BO7Pb(4{^2( z^#uQy=`?q6{8tsl=Ln~9j^vZ z^Ynpd{A=3D#32kmwgs0a49n5D?23*y2fgQ_1U9w@6z{2t1M)&vD1} zcDOCxkDl5_J@|Q@I}A>DY2K@m8L0VNb~!vyX7SFyoPQH zVrRO}`s+g(=kMGG?!Ebj6ZLUpbzGN70vqSj^p&WirbZV(Q-f~`%?DW!#@Eg81b#l` zq28bUYgAXKRH37fAaO&D{nYzG=b&iSuHCeF6B=Oh1Cp!F0GDg4fko#&iF`KV^K^KA0;|f9_WB zQ=vX@7h(zV=`w8nmUyz{{+JGFca-GL(@_@<wio^))~ak45~=s{{NrE+!FZ4?yw~RmYt&5_ifWJiPuqe z34ETE6hX>D9@Hk3@N;+}c=*>BCfN5K3%=K_(?xsWlg@Z#GS2&%b3B>ocj%P5NLxib zH#MsEqt9jh|06MEq=ALAm1ZdSlR+QG`dRSs)w>PxzHJRY(gxc#x7Md}{s!(3RMcnL? zqiGqdc4`Kw>9dXuYU}h z=P#CU+_hN3`@A9GMbSJXC-C8hh5eep9_Qix>qj-Z$~nL2C+_w7DDO7FHwUj+BptjX zh3|{r5yZAxVeqBh$=t`G5jyu7u7@KUp}%zrYCYc|g6C8sN1pwA#~}WG&_DU<1mw@p z5d5HUC6TL|>1Q{DaUH@t39LeB-Q2og&Ti^ zxqo6Td=$@gSSAIZjOAXbA~^1W++sEQ&yi3**ZfaP$`AHW^@c8h%}0RuGd}ueVyg6}LGGD*hNhHg&S)lVe3E9*2Q&OAwDDEq?0d;G) zjz=2JmHcz`4kmYc8D;3zfmcKG;ATMobEVAAXT%2bXth?vn1V?D=W4>q=6u)gt4zbu z&n;;!ohcz2b5jPn+a~b2u~7(l5wc)%TYXVqjFfX65B0?M886(=H3RNsT;}hzE}Y*x zD%7 zAy4p!SgsU!g*p1(Q|0d>$Ctple4n?DAj8^(UY+qkO8$PXtJ+qUOmyu{^IO6XfYx)v#>R1;;6mtb zX>MQ@=BF@fFYSO_8=CtHeqM3^mIhJw@x^d6+@9qCqo1`|>v8mO z8gRk(hc@m_!#T23)%58W;K$U@YnY6zvj32L@K8Ky`=H&-H*+KTdF($-6ZxfI~Arok0?LQ6x=iGXm=xevGD@<)rQDPo0w% zrSLv93;U$CejB65qj*9e37F#GJ^ZeL6@wp^Fd$H2%i39^Xh~<(6yU- zM9zlKEb|eojpMwn6wDRuN1nVmS;~3&W$~P+^3|X7gux>_y**yTU?S#{doRpv`X`Xr z9r@5FsGiU0*noPi#nn|^d!~?CZ4MQxX9+zV>VLzlPhCteMzZz275K{0b079y!8&F5 z1^AW^mObs*KeP8T0&eXIGSWS`)fCm&MeyWLH zgumZr&x;Z1$z-xa{P^BV)Ya6Nq$zTvs7@$8lJg`m2V4dE<(DnMYld&DSz#H+^~0FE zGXCb5ME)F2B;;hogjHK<|4RQk5JsBpd3$envhY(cM=tQvOQ|*%VWhC^gr7EWSvIeE z5k=a}xc~UEMk42XT|%AQe?sg+^zF$b*L&$7U6LF5OX{u-3 z207QdvKcd8C3C-J%;&@GE{C3vh7T9bBOf5}xy4dmUjr{yw1A}{cV_KR?$ZA z=VMOXSOET5rS8e<@Xo0m@B1E4&g8VxFzgV_c`ObI#BtqTC9w_WbvO7Kjcbq3at98( zY+rEl74Wo}KZ+52lxS|SsfzctokPjDAw9HP;=Vk&G^0ab^q2kg5|8@oay(pZYzo)^ zfrs?Ta;MfEoa6Fg>6V`G)nU1H$YW#t_7#cb>GRm=Xw;D`x3@jcXPOT_7k*8ci|sm> z!g(n-lE}B?Q&%mAjy=gD!*fm~@?`0}51*%aRUg-!X>#5__6N_5`r)kup5=a`#r3J+ zjkUazQBG3H@&Oku{=9;IMs+x%*gS&cIkPe6%~|QY=RR`O=6pGHZGHq%Z_>;D$WrL* zXnw~_)ZcU-86CmvW8mt`%zkh1ofbwmzKdLtm>Eg#O&V-Ld=QXcyVvZb@)3JZsl1P$~-cdQe7J4$%xoy%>XVC4U z2K>3@zOPMPZt#1&`fw5%Tsz6>HTvb`qD{U(+oG;{l1ikxzX~z$}7X4}{=CoA* zP$%JYAVmZ@?Sla_&P6t_2?8IspGj=kpfvLIPJBv=A96w|&L$PUbP*ELV?_7kraj=- zdFHc~5ptcFukV}?vgnA#7mMYX7t{QL`>2a(eHRX09_6_o5cA0e5;A6h+sOi(RGtSv zEt%)TH<6JC0Sce%7LnXP5_&EAH6>jHFQ*!MxE&AU+@X_XzI?+aBr9?K#^d7@{Q03i zW$#A|IH}g5u8vlsUYY|P#<~uFmQIf3xZ|;PL}Qu5*0Jl6$Lcd@&8;R9lJlQV4p}be z{-%qd4=kKrb)bI=pa0+-qL%bpO z>*iAX7J=ZI=J_0aACMyD`KoJ>(k2=V|H4XKEa1+}%5hMBkBhn&*(r`@=IL2Oa16 zZ>xoee%mOC!-1aEaQ$LF0N%XwZS-WT(K z*v8Mn@?7LOxY+Lebvu&J9qppHAD|B3&(1-qq)$qdNf3iYoN0sbb-`J9x<{o8=wZ1jDovuH7VohTj+yypC~ zZ!i6Z0H;22)#n+`LHj8StNQd7`*|z!y68XiQ+X~d@Zu*kOB&0gRXpbnxYE3+xdS^M zhEGjsP1d#rQu4A=x_(Qjm^bu;kHg0Mlb6LoM=G^3y8R%P%>O$2XO<&)oYX(>1M~=8 zj(UZ)g}z|my@fF|!2_nbD$RtBXl^k1yJuWfT9Zf;ZEP7<*iAxa)!o)ukM&RW*rR$u z2TAL3=<(RP!+hMj>l9_q5cp=Domx9;atilP`54Qe|A7dOzf6)7ce8?18^Bj$_vJuw zZiC0h&MEZ4jBh^(_qkQ8?M{8uh)O58;~gErBSQVEM*kVg|F;nM2AjXYFM<7c;AJci z*-zlUyO4`Qc|fkXG?{`UfSba9DVS1<>q{t|f|#X2tt=lNUU>0~s2q_tyZB02ni(!x78Bgrp~ zSI1T*q;b9<^j_@yA*aOdWb*Kr6VUJE&Ym|9JOJC!;)teQp=(pA6%5~ud}->p4*Y&` zk9{@Eflsi!%4`{NHF(nU+V*fFFUoZ+zMaB;o|$(ldHDXrsIIHxx!x4~$!99@|K8jh!IK3)mFc?fisu{*Urpl`B-0uEE-Pa8U<34e#hShIOfXme_~FYu zvt*)K_`Op(bn0VDO&9Hiu64=Qgo38Kg?{)*0!i9EEZ^urJh^bq>uwOv(?ePMf8GEO zHXQwYR(Pir67!+o;soV7LauihpwSRabnh7FgkqmEUl8Q4uz9UAl;;QnNBxcPWw(Ct z1*bW3_WXQO>koh3WxX%2&Isc=-}C5;y*@Ym!W_0s?|Hp#wBvaW_n>%^-0Ai+50b+B zwe7K_bL)p~>yM}5ei&q6Ff)_)VX@$!jy0*73B4fGal$u8;&p%T&sz#&v+<;7Ws&eJ zJ&5xFvEwDq!F(NzQIPL4rvTkx2~iq3m$;;29+$Xg_uLc-sq!rEvNu!Y9QcU$4Sq%C ze#cf{an4Kp4IPWdhjG6w^qb80J5BJLKSXd}fADn~Zil{(>07{KWaszUAU;1qj>ug{ zmv329RuS8FlN76Z$B{{CLlmW{Qj&1zXx1x38DD2_Ldcy@ZI*OGo%!d_S>KH1;l$6b zzmBzTEUEKepbCc`lj)ZFfoGumZj0R;CDD1dpsCjIwZs|5>GxwWZ>2ngnF(B1sJ>3< zhEUI&4Y2(PU*+FbI(o(>LHzgCpsu3-Kwslv|Ia82coMs>QOCYXknhk2|B(4aYe-4v zrkzKRu8-t(>ep~GUt{I81Dyj&zn4}zhxKtEzHNK9e4Oyb0>1p#KmEqi!;u{4*eNGk z4a4#Z`=W1Aw+Ye0obb%miXEM8Fh^_J@ZZaXaALcrruamTl&m|oZ|4;7#1BVnzgq^L z9m{_+M9$Nc3C503yTeGlX+0(BJ)Sf86sL_WPY}JGUep zhkp8d@Fk05G7{kZyNK3rEPp8*ym^|(*$($J#ltbTcl`?wASUYuP2vg2{# zM}iz!mUop49o&OccbDOQpNm0=34Eb4Cfa{&41o?gztd3@FX)b%HE8^W&NOP{&5aho zak_tcJK;@pk^klZ-WJVq15cLW23zGkKQ4yze&^s`d7vek=W;Tf{FtWoYcKp9*nZBA zAoKQUgk5%w;O`T4C0j@3QCxR57k(;qK9Y!e3ia86uNC|MeMR0Obc?rF7T<_O-NNeA zJU{3j_G&?2|s-B$KNZ)4eJ@Ne;Y9dzCWkzpGPYAP3-yW1rnX+KZh1Y`tkj= z6+V$o>QYX|qdzNgeVSfa9l7gn%|>_==W%z@@(YLQ$4j$II%eEoilEE z9M73}2;VlhHNT%#M{qp`cofV>40tNLN4%9pTm7ry5$Ld3E)3?iM^YrWCd>;b>Oa)m zc0)aSx!{fU*ya2zguz=K3KnZ@3vul4vq7c>4BkZW6xa+_tnM$ zF3pWpoR`%jn(N4Zz&DA`lTlCgHFdZ1gid?L$ipWZn}l(HGw4j&y3j?AhtBAu2fqFv zP1hY)CUTo3nU7 z%->}t>Q&gV<_rFFqi3HIPfz|2ec`a9?=$VgY2LTg#Jo@8Jf{IX znlh(K6C3c5x1T%OacV7i2Rn`Otm}E+$uZAR`q1p^kfRI3#Q8t~jeBacU^sYytY5tl zK!L5N#J<^#`ade*(L(eoEJqOM=!=2h*1Mzb72Z?)JK*~o?=Be|hI1aDiHJ`u44{ng z#BPr~kuTlBtgUgNgt{J0N=$6Rb+4*4+%H@9CnOw;;&T(Mmu82q@AmH^A^nl+!w$gP zogy`E?U~|7Eg*F9AM8li|L&VtkGaFDTc@gCzz1TzrSX^CTcUr-YWUub8T8>*OX%1O zs+w6BN0G&GuYva~!FQ<*ZZ#Tu<%1pu4^Ex%rqKoq+ycS(TkSUHyQd3$CWYLj-s0a) z@SU$3`EJ4z*C&-@dQ~J5_dS#jmr9Y2q zc)dYFjDHV3W3wl2t*&5x%J51XQU8j0!PS#S^RzyRJ}K4_^knSn!CTN*Eds!oQw>{rZ>H>}U@IWEwTKHahpd1B?u^ERNbXXjZJ=7)V>=gd6@AB(w) z+@0e?>CS*g!!v&RlX*(%189F}Y<5hu1r09rx0Cgrmf+X2d$30cr6|Vt?pO<-ZQ0D1 zi=Z!NbGvTdw0yIp>0O*>^AGOzTae>TOp>enK@YNGN zsIz9P?Tc@KFIMbv#i~xqb6xdfc|9}HmHRgB^W!;}a`croe)cId{U~?&i7mr!i8)kX zLumYM<7wK4QqETy=FRKV=~6nr&frVk0WYq{tq^td=s%cFsb?(j6Hp&BKLq4&mH${d zqUlCYo>%oTj1Ek!by6lql5iYrfIs=prh2EtGWc8x{L(pIeE%N7`PC(O#-MMmbZ_$F zFLQtguygHM825z)&c?p~4)|0nY(k!(FAlpgT4jOt$o%7O!lzi^o%PyDsh!?Nj{gWB zl#pSg${&lod@Yf$6ffc!hk1VIW8l8*y=IBJcksLzuXKGhX_eW&EM6jEzn9R@GyT|f z7mk+zCwJ@E;_FS|;R}CvmDXxv&UO3Yx1N2&6nua^;Hxh5kC%P(Y~}`YJP9}})9Y?> z=RS7uA!51g(95xV_htz9dj-Fg`B;VHeF}WJIp9?Zez(nCc%J)ODeuG3hp@b%+uQiP zp^v;j|EGQ0yTZpI;_t)-p}elTi~g7S41>SW2#wcS_&*7sYcJ~j;S+}S?R*@3!zSg) zpX21D9O5v1*BWo`hll>k?$w?wbD0xeXtbwirwTuc+c)sA4(h12b=SLo>=erTN9?;@ zVa|4+-8^ZdS+k7y;8}Gjv1#IY#GCWo;3F|b&jU(a#F|Z&4WXFb(7_y+Yu}EcojKFa2#W z=D)F+V=~{q*~o?cr*AbCdgIgDgL}Nv_N0QNS~I_!xKq1Vz7zWEkk>eobZ)}B+>bS_ELH(p;{Al0zLD1b~-s}dFT3d=r6JbcbGwYs_jNJ%68t7__;FtT8t8$A z^8!A<8p0R+^eeu9+jKgH|9roG$om!Q_A=B-O-A47vH?6DL7$Z!#_N6bp-i8%5IlvA zT{G7xz(ckxy!paT%KgfX0x3}$;_VHcI?EAt1wU8F2h$Y$kKiDl=YaZsQnJUJuPeOa zQ?h7pX{0wjwJvDhbD}%>X?oOjGxXuP(7=^0HGh*eu^PDRoH=#l;zU1z!M^-GjR9`y zRp69yHkMA;_ilY1x^Y$?KNA049&$<}Y#;cH5q*LCflu@`V@pTy{?2?J+D|`EN^5m0 z7jFpQxb3N};Hy0j$Xz`^oCAN7@EpzG5)vMl#{MwQF9PqL%_}e`WcwvvJfF%VIPYvT zbV@=_`*wfYnXoj#@GNj}!IwBg%Ck;FbXe@3_H^cX zF3G5916_V=oWSR!n9r0|KkU_JU=Zb0befdy@6B;K@B-OA zx67S;9WU*S-3DFXbF;1GucbUU1GpgjJRQZkC+eKkzLnC5peWw&fEUuqPyBekE__uOPxC%}(%oHtB>*?8 z{M9;n1OERkmp&1=7VI~cqrSU2;Q6CRF9NB>K-GfsKccTH_|eQ43-k7Js}n`*KgjrV zSm4j=1MIt5AZR46K;P5!{@HxY$1T+J@5@7j>0@rQ?4y`3Fn?Xt1#G?t{wT{)fL?t2 z8`;D8XWYo~^RAaM!===%^_40cL-_0W-Th~p27Hlv{Tg?0H*$Y1x{lp;O3ps_Rm`oK z@9ub>6LJSQ-I0lz+6gnG>C~!;mdq zaJQ9r0O`HU{^a}_b4J0x^$~PeSrgtQW4*BXT|2qRPZ#^gsp5UMJcRpM)r8WwdD~8p z2*m!B5=TJ)FnK3)r5?{b~f`;jkh zVG=Uct{{@xpsH63{~n%YOTP^YqdC(IwcZPV9iI);(^w3fd%)*&ZN{}yRseM=M8 zKJWv-CA0nfpskqCFkdm?It+)h3g`XL zPvjte87c|#1;6r&Q{lMjxcA1K(7pdd#F@|X_n-m2%aU{dmiwjv#~ZOPHWKGF zahh14`OrI*+II0B)22OqSp~n)shC?elFMZ9{bGFWVc|U2YO##-5HW9N`JUj%PRvpF z{Q!PF^B;r0h~XE5u`UH4@?^{*;>NT(78pv0K5cb&?koDczZT~^@P%Ri!O#~6DG#q1 z&?J=WI?*Q!>qp}*@=k<(dOnEzrFe&P{w8pH#veo<&v*m4_kLH!c5D)Yyapk!)i0EG zRoTzzDU0AbqVc@`Iu(n)Na#bfIUY~>0UXx?ul85W!?@AF(b#iYBHmY_a{l{ni2h2u zkvmbTJ`y?CpKiHi?ddi})K{P%yFT3LznQ>Wf8SZU=cP^zeF)LY_4f$lJfdm|z38=Z z$^Ie84Hx{>lVlthIt5&3^_COo{sM0wxw_T)pYWv=eE+c@82?xTeI`4XWSsYLCy0zS zybhnZhxyBynaSg(Msl6n{0N>$3_j77d98Gt_mGRZwc=dE3-{62DZ6#Taqq7FR;Du( z`ATEV_9cRE#dIzY{pj|(13L;QByj&h%m>;03H$5P?%jhI@4Cr&?_^(%5vz9|2nz4t`y(@6XfFxxl+#~Xg=)nbkg8A zX6XZ-7O&&e+>qZW@Quz!(DhYqp+tgDga3t9>K))qK@a$03;Mq+{@lNEWeDj<1$Yly8%BZW zhP$lZ7)|2_-0{5hPsH!Rvt<68t^GOAbOiW+g1_`u;H{nioH_Rv^GIp+fT_)(%iT~L z+ZVV1)6Kp?|03|eXGtj{by8Z+qbORN<`dDfIEd@Fz~^B6JSUE`&A%Y#Z0&*WSKzrr zhs^d-fsAHn>|K-O8Nz*0u}|1~z#MU}povYzK($guLE}qb1 zuv|*`9$h}%TvPL-7uN%y@!~!YPry${Fxiyp5oGD)VsHm|%-O5BhByW{E5 zt<7A1eiP<7Y`?w<<$PM4Z!F*FP#DK`kILzGr-M7!eh#1>1}jIXa*(fm{ZI4*@T-`A z%1-d!1l@f*=%)n!{t$mYU&lVL{bmzbsD$5nyKga^N*rdcKKKRU85+{v{G}#2`QgXVouKLfOB|XR(mSv!Us{9 z2OIi>zt>h0G3GVulK#r!Rck~);a4*161BSTAmGw$-}e#c(c>feI=>dkb@f_NoG*|s z=2ouG#-?Xuuqe;rD&Tx&H4k{v5v`zuvZ8 z*~~{W-rpeynfXGa-a+U;xKFlwe@A@v}ZB2h?{J1&zD#CnzmRQedxYFv5`TE=2`SaY# z#qdu%dt2wxhfqp?Z@#T3a3<~B**86trF8gK>5N{eCo`^w{(NO7;kZzvAZl~`L767* zZ^k#VlJQ(M^hX=57A+~3!*?y)Z|e-qac{+!6m_x@`{nz{yTh_^YwJV*B}}hA1RnZ> zSziVnA1dw(%=d?lylJVtDWM_1yM2;v^rp*mBXv$i1ygp0GyHjdISvUu+J&n_rx+L^ zm+pQ`{f<9nq!_oN(*x5eUY`J0Vb34)1LNBUwF|C=b3g3{+R-Q&WA{Qbulb9n$uqkM#I8ZS&qVFEV<*X~z!Wvh4I4PACW`;+)oC5jp|0#ke^TC*COc5 zfy;W@9yYmbD&{FVqR-g$y;>XdLFT8r!jp75`#w0LM$9 zC;XmevDM=Tcus=>fc|+Px0a1MbFrHu}R4?(FuQ%w+g@e!IBn67Frr zlaQjX`Z_M#Ej5UOOg4VWibwxpA2L1}{v<4a3A{MQL%{Q#)#Fu_F6J9-E-@G9b<J>><`Iir4knm~`ia*t}!mk4~W>+bY0%WvhI z628CNU&Hw%@Q4<9@q6`J2(3IBsWfkZZ+=1Q)NXEGyk6-lp5H1d)x;cZzZpNj>u@A| zI*7O??!$|k>C4Jygi&WL;eqN_!UH>)Fx@b*bPB zAw#}I=|kK1KA1DFveFLz1YK3?r?gzPHz}=*({r#--xc{BwBHuR?@{z&%zqRNRB-0I z8WqLsC(Pfs3meoYnxCWKpR@Y%4SYi$cM04Q>d)ul@Uvt0rXALSkR$K_xhujPxpOF; zd}Ql#Wkoc9ZqWA#+qr+2NV+$!vejqgaVFP|Z1x&+BIaA|FJb%Y>MAMs_r-o<{+ba{ zr1dPy?A+{Vy1qlB?MC>S{7Jk7T?)<_7_^N|mC?$a`+6a@B995YQHB?riSbm9hp{UBtaS zdfW}$N-y~H8?2NYc0w+X;9p@3{QrdP`#vktzXS8+$d(J9zv+W>Lt#H!3w#nbH;)wm zdmcGRrdEs3JOOSf_-cIj<2oeZJ`2p(v{M4d@NuZCIz0gUdwElXfcqiTW74RX$)f|v zuiJ^G`exui&a2(?*%`TUs^*J)F;`01QTWXSd^Fa7qwZz=+=>4D`wLM&2|f_ek+S`D z0{o}O1G`V{EXOP|gzvL`VxKZRlJu5ro@I%1NK44O=?fi}z>9o?oID{H<~@941)qeA zn1{SK{j1K!IV1FGsQ1|X6gdzK7svS-XgzFavj>=aPQSSDyes;W$rE0LmiW<~R~~&G zf$!zC)EU!!AnLyfoz0$9_;McStZm|XA3_DLUpn+d9l`cBo;%YWy@9?#nCrsVdr^lO z<%%NF?`*e>_MaN|wz#Q;R_-qB=)74%Hm9$Am^)46k%U91BjCW$_x!QAyrrTfn*N$p zUG;wezRH+TtE6=BS&a5KeF>jimiLAGl=Z_-eC|B37JfP@ng7+mAA;Q%z_(a_7yLGB zjINk=1|L>f58JG9u8%Lwn-J;7^*j0a9Cz?#Am=zQ{8j55Zf{kLhyG2N7aTy}jlphk zN8mg{enqDUdf{HXP6~e?#?yelft}ylk#zBVy3Nbhv9z(^_rZBJq8^|mlx~oG_>B>m z7Ye?#;OjHqb7S}=BsHp3%i#06ZhTSgY2XN1TW+>QAIS1xuLRQm5of;~hYp&}hvwp( zf7!NbbRcxI(CwW+B;`1)KX_gz?etEc#C(g@G4O*pIze;j5mDz{Dy0@ZouhHO52*L}ub-rg&~*Awy= z3&g+kU=W{6c7{*Ge%azjJE8ljXuUK~2RgtWKf5HAN0I+k-JwyquNdzPerP*yrJ72hRBV(4EgQTS_=CbRwMcPJH@vCIa^k;P^=3 zqz9}U0>;*gyc_6W7(W5HI^$Wd3gSLapW&xivM0?3?<-=8)#GiLf8I`daId-wdEwWy ztRF4_zNx>i?T)+R=lK>$Mw;8*n;(pz80}^Y3&;D=n>5P_i!X#yj`G`&myM&TmCb); z0mH+nVyDTsMBs#7?zvdBxf{%RB)HcOEV@550lsL=&+sk$TC+o3P*W)5;#8zh?8i)#3a(A$Ra%ZW}Ev=q{Ih(p;Zw7s=OgH<6Ezd;h?X zhZFPSL+N2gTaWkWp$`}I;LngtS^Bro4D~hZm!bP+>k#;2yJI~(yqddHynUZtUci%E zNWHqP!uieoj_$*6eD#2s)C!)P895K@UdU_M;76dPp112Qhu@mD)5?fovb*l)*A}^cwtar>-WWPo+X{8E9B;3mzZ)`%VV%d3pqL z!v+1!`T%}@ev2Y|-{0{e*k^3N-Ul!G$2yM(w?%#;bhhmC972w2w>MF04Kc5=x0s6z zf7Bnk=G(rF@+JH6FKkuF#bEb*f|MkB18iob$w}Jy(2}K>_?$!o_!aY6L>OE{M_e4=QlvH99=)l#}Y zI(BQ&7UVU3(b34;6!Id$V_-Ri{VB&jZhg=^INtD9kI_r>G

U5Uwf-kh-%XGkpQY+dM|7W{FNC#109BO;br5OzXQpnaCyQy z({Sou5FNjVJotRRvB-Bpy}|k}bJR7SP0E#>BdNR(oQ)m%A{W`z9wKFVlSe@oZ~67vZY{ z{`<+ssr$MFavU~0fX`KcwGs}AAZ zxcstd#d<$dy-hybE(ZP+cY7UkhW}|$&!l;mPw_gaBpLZqLcZZ?_hWe>a{txYib(ROGxXJ)h+Xc%{(`80N!WEVMh0X zCwJQ#r{=&y6cM-jpKM_3OH+oIa%f>%4VNkHxr zn5oYzcGS}*6g>r2cx zh1^QuKrC;(B9tcfczbPfgd4q#_m#=)J;@1%GWRg&D7|dksq+4IzTRBnALZ4nM-S8) ztbZ)n!SBuE;A2Ej^!kf?g5{sj2&74kR-V$-3gWonx)5rj9I@?%myDm&&qL{o*OH_0 zj+m3}EI+jSgMik52SL%$ftp)!g=n^5I^c~@F%fBU(V}4@KKpx9r|>pxA}$p zc=4GQ-R1)K7V4in$Zr;SvhUzOB>eskf)5%B7T_n$cfb{Ox1hr`3FP=4aG(wId%sHW z4L^IC(f9E+Vt&cI9W-?Q0y}>>bQwa*?-0FC>#J@x4_#d=Ig=_^)th- zY(uzSD;ItJf{pVB;<>Q7<7wn`e?DIDTd~0Ez zH4(f?v!}0L|MTGby$a-SS+ACdz^^vYc>VKJ!0R-GFW_c3o0=s{hd7J*3(&y`Idfsq zUFe;fPr&b7NB(^j3E!QyU&0O-0=HP$C-YkMDd>TO{1IK`FA6zfz^!UqtyA>av7Y1M z=mQItW0R#m;XGfYR|MzlBn7ehyS5BDo#Ds*hTrm{AD0!avr*?WT`%Tv`+X&0@%mA8 z!OgAcUS%*z@_+ttpW{q5vn98m4ckdYUOW1P4e4XhHCPom( z60MyMdsFw48TCI~!VhtGNPXdb8Ff~_A9by56wgNp66a=}c2uiu7ksUy~0GpW%gy@@^0&#G;v=xS!R379OohhTyS#njO2Ce zKKR8h`DAw`7rs-eFY@GAZ>$e_g}(4lYu(Ky;NL0tFVDA(pk`;@yng*0eWZ|&xC?!P z?YP%Ty+b(eWeQy6LQvLT>_f(@n+~3}{?~5#OMyQ+ys3YToNtzcmj?WG-q?$Q^TKJT z*V(ZLaNexRnKw9PtUr|-4l51r#QB#mZURpia@*{Xe=6kK-1eix-OCrec`c*e@n>HS z!n}4=_rNFdewg1kvpP`>p4j$d=Y}2sg}-<5!Yi+X#Qefd;8FL`$@=j=lIM;fZ-Mm* z=trU^-ncygI%plsy|P1+DE_%^!f14n_OBt`W2n>9gZm9`M05Y#bK!iQAeYs!A#862 z?rYYs*dl+@)bBwe<|<5w^9XwWYZZ2(3qv`s_A`iPyjM(KwpqsW-oU4czC12HXPgtC z|KL0xhAZStco>WEODM+{t<`J8Qc=KUw;aBW&0^cX%5e5f9^hfNNJ zbAMjwj#=(kDSW;r)j711yV0w+UpBUb-ptWLmNCN~J~e{hU{L_&_kZ!`+tc-=EO}v= z1U#Cp8}xm%pU5XS>k!B14!|uL?q&afJkg)LyO!>beGC4DP){BWqgzfo=6k{;`FZRO ze#o8{%62n=&-PuHqye0Z%}sB~Y1#W>w_~@-IFG;uc)sBG1fN1S58MPCM8I*uFJ*iJ z^p(QjQ;YiV%+qUgKXebK+MF(bcHI&2t?{T2bE1-x#zyo04Ci2Oqi?^nJi|E-f%}H( zhHzd`N0TjI@$VSFPw7CnB7nu+1&>SMTLAan{4oB-WYk?PA0(|x?Hrv5Fh_bbBqXf{ zemLxT0++u(XL!nyVLrrodjx&Qqy2VPSXaV29+=PPw0;4oJK)o2Xco$OGgvR04c%tP zv`4=-H7zP|T{zcK91iDv`%$5s2XM}V5TDj`6mSZ5pLIsPDEMwX4yU@|C6d8iu)l?Q z8T6HAtrJYW7Rae<^O~8)kHRSQ!?P-Va_2e?@Do|i8+a*|%6p?M!N<_VNrHdnYWO^N3B*&I2soFxYtHJ?9iPY)9(GBO(R`AHP)0ZG}1zz5!bEt#2oEjiCn@$~1j}YY6+9G<<2sIAieu+&6(BE3$k-&1M*&m>5Og&1g@jCA-&opni}2=oo863}moi3l2 zcX6nVIV*>TU*CV={F=>JJ}k@I)1es`hMmIJ%Jw*=lE*R`)tos6yH zcQ_~hxuq^U8W=^k52O-FmneQdLodO2BG4BL`(X$8rN0h6R@__<-wh#;19Q64ZPA|H zLXhj^>U-&?br|mt8i(^d$rGXM`PsdIepZY&d2N?f=j<>}K3p_W>yT2~jtC=s(MZh}=JoDxIcg$_?PFdet*;ed>21?l9 zIo}?Bub;!ucY|L#^PT#G{Fs9~_q;p|Uj4Jb%XjKT^ZR~nD9>^5gfGd<4h>h2z(2FM z%439?ob1ESX-%E&MCt~+fe+7tr+%-O=1_S&wc3BsrEj6AzlN`z;9vA~U=&q9S+yca z-<>YDalBFjyo>3SKFVp6%fb3W>?;ADo3sqPYT+Ek{_;7~Wl;N45#N%cP7&~P%!xJF zmp7d$sfz1`xhcc-;Ag;a*|ZQc3?BUQF7D^jGwt1eyYu;2noLfiH5d129SNp3Fh;-f zQ1of<6wdQtxB2jXr<~Wlez-Rt{0v;Ozb=-~qft-huIMr&61)zU`yP&Z{M6o(LNnkp zg8mWrTdV!EFHZv>;o_1Xk5}&k&(i;TrJL1srNy81*-{7(aRm zaM)A7RtyF{v(PR*E32gs?fsip9sEc1$w$4z?wzJk&Io#SWdvz^e9+H%j=CpcZKE~I zpa;D;RR1w}_Uya>UuKl1SKhjKf9}5mz7(5#INiphtLmpnA7)l3D%9qs;?ct`q44F zg$sTpf|qR3;e0#rS{Po}l-EV`4WJj8x^1G2Q__pjh zo<@Jx@H>3ttvEg}O^4rNK-8o=)4Vx9&WX=aOW z?a1A0*~Z~l!Yk2NJsm%9#dp8QQcsRYIeJs-tF^)2JH+`4K7aGEh3Qv32b~<*_3f!~{QUESzM9oXlY`lL8=5QSIilC#Q&wIy zz|BBHFC64ay_zETEg|sg)2+y@6a4GoXT$C#_@zu!w>CWS(3=Y0%)CDPvN!be+R91W zyg1(&e!J{BuZ|%Nld#Se>_hk#;vCwP6Tx#ApdV*>aL}l7(P7p`lU0bb_c=6}Av>IXmA6KY5S_RYA08>+A=r@^9?5j}D+c+okj(-6+7j)s}qK>*=)StMcPh33xWrTeUoy=@o zvU8!7=Re}TGyZ%z{GjSAs;;{M_cR;I}CF9A$#HqXAz&+&_06 z?mS6(jJ!<2-y6I@riVhm!f?PC%!ve_;Y#S%Ulk?{J|NByKgIIrVjM^t*8Pq$fj*e& zAMX2eet1GS=dB(H<9dzO!Bo@hW!Ujx_;vm2w<5Jg7!{qkk(ZZ;e5#wN4pXA}JfB_y z-k_>VUifZ`?De3FZ9@P zU#ghaBilPbN^>T=bk2jX3*)EO$C1IUlxM?#Me{!EuM0m9QI`*%vMqkY84>5kT$16G zb44Eg=5UT93<6%>EYs@w3O_!d!uw+WB70EJFh4|b{#%7yFQKkR{U*!{m+FYQ5#YTt zoelhim|xmrj+^-vg^U()><@NGMHOJTG{@c5;n0ure^CsQqcR&BW7DfBd?Jw|;iXpS}wSS6}MLh-h_TMIU z_`XZim;TIKnmk`i!e8a7OkVOytzeY-MSjUNlwDS!rE*$%oHRLPb7l1@zK;)Af7mVD7TsV2&O5 z%ww*0(hOS`O0D4WE{ zKJ%d6_z|L>EJ*YjTNzG!x9q+8v|S8euXW+n$>i3%RhFULAOEnJiwk{d2LK3%J%J~$ zZFe9j7Pz{gvqL=_obRmlab^Tn^oUPcI|+SJ#E9T=YeT5~(f*6(_rQnMGwOCQHimiy zwN`sA4dS_^*WiaM2LtbKWx#;^+=eh>`md(G1Q^)ykE0u3Coa(W|L_Yfehr@55N%!E zwV0cVx;<8BLeIzasH1}UIg9$CF6i`x*l5v5Wdm|E1s}-T5MHOl=h&&s9otntaWqmt zzHTb!JgiUu;KS$ln7^@{a|!hN0?+-X7timRF5;ubIOlc07$}g(!PYB$!5AL^z7n0z zpSqy+6uOXKCl?HI4x^ji^Dp)MD&aUi_>s)V%RiXs75cdI`mc|iCKWzDa=}kRZgv_K zQur7%Jn0PlG1?A3vkUj`(av7#*i+*C>?`;)f*&aEqpc;*$%liZNdJW6>?7zW*gk8F zJUGFB=q%1Zfv-G3%5}=nL*`CiUQmMH$8flR{HwEI^)d~UxmnN=g(z9ZIRF0{rI>lmp$S9 z-UDC#@15L$&Gr&LS1HB(LGTF&{0aieL-S(x9`#A!Nk z$%%1i4sVjsgFe^p?p)$avj@m0*_FD|sNE$D_{sZhx%btKR6_ z@`FaN1Am+OX`)ZodmNvjeM`wduPJ>r#Y{UqVLg%dj0y1 zdqc>{h=Jeb<$dvfz?Y)-t!eH&KbqGkE#>4s;I3p2aEW~7oohcQV?OtL+vp_+{P;fT z2HfTIrcus?5~|*(k*L2}PQ%CN&XctRzxd{^`_b`HG;qS7y=@l*j~+F7Y=BJ&b^U13 z5Z)<(o{qg-O(TN%Tt$LhQI>m&pCg>Rm~+=Z?mhH;choU&swfydcc`OsXBPSx>m`%lua1Ww ze4kSeTZ5^%RAzSxzO*c7vJC#x?OngW`|i$p0Ec~f4!utj*W;l-OPzH@>&AlsYI?xL z{~6{{j-xWVtU(^b{k+#5Z9hkG-ZpaBnUBF|Uk{-ctndG7t^6j{WErc!qo^- zuVEa`eQA5e^85hQY3$ypMGp6mlJ%QUhH&0m6!=X48c%KSCFTRbSNN(;uc=e@W2xN! zd_|B##(Anoz?V7lW4FyqKfZ4<_Y-(k>!BNN9~e0M2zYbsy#fChatf?~=dbR(H*d>o zh%S@0p=?Mo(S_HdL2g3Ut1d< zbQE&~FgJ>eDgJp8JcX*Zb^blqig~`^2eI{q^Uq(JvLeI-yr=O_2d@_b?-y`@zCPSv zc@1#ZHWphdTY~@8?6%=#f017dU;2?CR6hV;=H;E#YI2IDh4m*-96ZVEP}OWX?Yr2g zFdsZ@R+l{&{X8v$cz!B)PYfq3b*Gaz@@Az}d(+-?(fi)+4y1(>t`{Fh{xCbI(I4#T zZP|GR{480HrL&CtkDm0P$zh*Ml;C~#%y=?1XPSf#B&L0PofE>}KjuS>kMbRQwb`Th zFMAlq`}q0Lt2s>E{ds7u5c6itai|G^L#7d-x)`aA76w1yU~w{MeqG7zjhua%U~83cT! zo6%Z>2N5JK%8h^f&Xwy4Cx%dL@y`h!av8Pi)8D57_++8{t(K>dd-hT9sPdJlW6Z?7 z=a{r}UO*^?8XnvdtOFf`MNM8)%!ind%UR5wg*q%VfZx*_Jvcuc`t0A^_Zs_y7gID~ zbEk#Dq4X~POzZ4{34E@eC*ki4___7pf3qiD3#A@6{<)cd3Fbf7Sk&Qw2hMy=FvqyI zFWt>8Cz9tNf$z)ScXALVRQ5f%>~I+810;(1%9vAJ`tmHLAqRbl0HgtLWc_wO+%rYU zylgTtnc3>{-UVXb%6{;Nw_0_-@eV$|Le6iBobx{?@ZV>r=}7}TUaZ*$e!?2{ z+m%1Si)*R6du!%%_;wHOFe+)2=<5QU`kg_y^6ZE>(hqJGI^P2Q$@q%=$?#JN%~)1$ z_ZPX44%uNt+@onix8V9o&!Z^7eEH4(@VP!&x%0qL=yBPcp5Qmu$^Z1^wc{4i@h<;qSrL-8IxjciSX8VUDCcbM4jF>kEBHh=2aiF>BL$Am@;p2Gld<<3NjEotN@+J*?>qE< z3}@VeJOG!_Q)SRUFg$#LKUKu{DH`1&n!i`%UNXP2LkS#*^p)}XEcj7^?62t_f6!kRGf;#kfW@S_8t zR?gnM2=$tv51R|$0!O9Ue~tZUdQo=7vKjF2l2}fgYYLrUjb&vT)?JPD`naE35qw^D zA)I>kHr3jCFoepIn|d4x0&h1fXu`|=&^aFXSX&ET#LIcRubYjBUz*@IFxHEmGtT2s zSKPn*J*~i-`-jMV=~8NbpgJv_>&0#b@IKcnkn7I&A-74WkHPnr#RBjC{ayIz z^j!wv?KJ&)!Ed+zo|)3c9=-^=r)Am?qiF7a-8DV?hj2cwH_wGw zq!q%?Ipih$b?H56G4yOV~jJoJ?t4=u*rjO8W|k&x=bqV};*#d+m!+}nEj>N5#}od0MoBTw_fkXz!BJQQ}`0JjsKr(Z>ch@;53ZWI1u3=ipn zduMj*#)IG^Tc+Q_x8nv8SAPp%CSg7RepRF2Z;HxI39HsbJiN)KZ}w7z;z^YYCGd%YU)j2&PM}I!tTiiPkz1w7Z5(@UgT{t z|I>xI*9G0favw_BK1Sc5Qxx|xe2@E7;HRQqKX@crc?OrtsO8O1*qFuKiMRlCS)HNF;D?rjG+7Y z;lpv&F@BtH4V~@xc7|Ogz~hA9(-ruYZSkii+g~DY^~w63_nw2#Dfm8vAH8kG@ZQER z;`n}>kGuh)55WE#yfLNiyZ^j7ucDtjUsvEm-AC`+%MZB+n{PO@*cHxm=W@kdHxqxl zGNi}20^oLRuG~?a)1!Ysu-(!q_Le8_Q(nsGMzzXv4Ay(4ZO4pgg(IG1nf183 zx;~0l{kz-Gdp+(WLDy7{_bLQBD?{jLt>u(I&~*vVU*oET-;cPr=JZ})H*PZWx}qH; z?w`QV-M?(c4EPO>y{*@2fE4}y`{T`)11DqOcTKEU;8VxyR2S4&g0CCuN48GE=MOv5 z!y)QUByHFDll2*XQ(3Jv>Ltj}6uy7kUCwWRhq{H~!;OMDeyNJ!?-l$Ec8;MQ7W&$x z6p>d49Y5=Pfitr@u>yWfLQWEL+BMH+#l(mEQlkFRtTdcALxTHhrUUOYJoI?t5PeVX zci#_ryTZKVtSb$O^4VJfz95^|fd5%nT^3_-Hi+{S2%%wG8G z2>nop5WX+)cQRdL1M*=6ei3qi-~G!r9)Nmc!hr?RMc{vQ?mytvRYN|HGJ>8fV~e?a z8`L3eKf$lG!}wp57w?eKMN_+!c9-4Ax_ipO@m}b&gxrXiB40ON%5&j6%E(Ub?Y_wr z{Lk~T_di01!F1brul>edY`?x1K8d#T0z7!-z`^nqOKaAk>_^sxJbSviAi8fxj`cghu-UU8Q{_(!GAXq^Q-*B%cp)y!|@(Og>`)gXJ!q+22ab!o;EiqT4+=J_9@plUQQR4#U2QG7*PG!IkcfzDY?r+ie z3puLi{VB87AyZl|CBH*Q7Z%AS6j1!}Lzulc|32Wlo4sdFZ#WA76QyHzrwiiq$2wvB zo}XgA8vIcYJ9fH0%*B(wf7AZi1pK}ulcO%xx{CKN&KIVq0iTZX9%9A&vALMT0X&=G zf(QJm|JdW-W`FkKeJtieMmI+Mxo_>y{b;tjQTvgm`B#qwaG$jaz?lTz%CiXWlZpC> z`Evr7I{te04>l&|%xwYRW84 z!|6q8hvXs~Uz&QmX`6nik6HhQ{l49(QSJlqIOnd$WXU*;|L;9Tf9E#A+$T&|^v`a~ z`w@*7!E~xr{bPWUH_a=YU#7hzitEcUXMWr%^}5cD5YF$PhtQMRjgB)(nEKoAD-NJ$Sy?AvxD=J_P<*)WjibK^P4^ z<(=NMUgVE=4B(}$tr1$X3`v=)7T6*2|lY?4KU0O}AY`Q9*^*WC%OHqeIpF`2;9PzvtjW`#~xy zSvvTXYZo=g6-|^PFDZeVe?IP%U8SJJJP@s-R1|QjhsF4%3ikb1hbL0aUDxz87n8UT zrmva;YLBkbG*6(1!=}IL+FQweV{DXUF{a0+>>G)+!<;N$_EAuuL04MW$yB5#c{O57 zZUXyx^iNGD4O=otH&^lV(nQ7as(d9Wod5lo=N3=DqYX!tJy+7s)$Kk?UnqEeFhb4O zMV*SiUpzJXsdWOi4J<0%*CC1f{_jyy{mkbFXM3vXZralCs*_5xO>lASVWFa!-n;z$ zT~t(5dqiGuxQiBA$2h+^uAqa@v)bP2tstxYeIsukSJH6hm1&()m9$ct_04F6lGhJv z1^=&^iu)7WBv49sOimsuD0lJQvu6!el=bY-*BAO23zV!z|> z9&H!(ZT3wib+J8r({qNBF8_<{P~BP0>Wdv`RkUnqkdsr5ijup$-dcm_nHe1L`SN!q z&y&VF*;Q-Z{pM#S6-=o&DxVrpD}EUl)y`Lv=VO;A-SFK11b=%v^IZb3YgQ?#xtC9u z;p-JV*TPvz@xFso!?lvQ&zvNY4sK6=UV?R}A$-ZywE95G^G%-stcWow4)WtfzG17i) zw1Pe#Ykp_VaTUcq39~=0l}L)6anb?HReXQIf2nkP!~C~d3aV50(ABMvr>q|)_Mb=K zeWz>AF}$QS(eqJvsa#!;FoO21}vuMb|LafKt!{062_$z^C_FDO{$Xr3CIgM?EgkD_9gf-02j=G7D^=&+&fQ?^~o|Idc8}sA%kn$cBot1o~S#H?H`plBQSZ?|m{s zNq0=W`nfzyfFGvW#EgA$l;k-0PE0l5@2BRgscV%=VV#peJKAYI_k5tFRf{e+oNTP1 z(4A9`ys%W#ltqEf_J<}?!-6SWJ+zdB$8&SLu8Q+y($uv1U%t6@vWjx6Up*OiLPfeO zI{Un6qoNk6E1r<4iYE8g?soc|g8NTbDQR2{tu?^<9zM0r^BMyM4OmgHn31EPZ->&S zJjDA--oDi*4d;vZhavN?r7Ou}G&$rstJ$3H)B+XN?wD>}x+#HbTBY`xou{A=WoNrS z^iWd5ZL`-&W7Ra{&yY>tQoio|dMe1}sCNCY2MUTgV|qWLFV?r9&y7=%O{=uKE03v3 z$Wu;Quj07CjRYDyEW;{icmkg%k5=&d{DhK}o$Y#_IFrD0TL&rW*~Ah9#SR6vd^+!j zvWt>hG%33LqfZjGd44T3`$;0t9q~}mlUIQ|AI*=aUS9q0IZaBW7L9)CJn4|gec!N8 zGPa(Z`(dnt{%H+a9M?ld?!C^;>b*`y)4RWaX_TU*e+GZ*;+<7=sX(uv^A0up`&-xH za|?LdA)L#Wb5dGcs|YqlMQw0SsqaMw#qLv*o0sRq`Pg^L;hGyQPN^wz+4<7+UHrYz z*B8&1b4sf0aHclsm6}#uZ!O=Tp{8jL%yzLS5TD#&zzU1WMl4^l0UO3UYexxa*OepZ^->iM(&kR8vu=+3oCI zDq1hK7%@VrB%>v#e}4R{qK;eF_Hf^+qNz27`z{Sp(F@6@r3r2J^7mY>Bs_+_LtElJ z{F7lX+p6T}Q42NIwAga6HP#)|$rmNC`zro~f_n8&|2Sx#g5O`3YJN_RRFYr6!ONTB zoc*&;)i3?8lJer(O|8N`JErkhB#S7i=;G_`V5zo@T40k=Dz6!=9?hEzxA|yUHXo zsaLj39+OC+DWl_B9>+b5^;XXf#-*e)TZ7H4$)A_6LcUk;u zU)&q}3+Ii)1MI_-wx=fG?+Cs9wWaRK1kOAAgSuwLp^FpFDCz#fdDWlg3bOs;Fn6wb zJdMhfJ__l}>m!XON}wo91M^Dx{)+mgAWKV$No9$O^BGKWA2fMCwrw@`nec`C z`<$Nlog+?adbuQ}LfCg~pH<*Ishrv1i1X?2`QoWr*@^t$4@>0wYhyK=e~!m}RHI(v zY_>p2J}))u3v`t9=G!H`<2zJbzl!^X@h{v|^s{{D?PuR`&csZb)&~79t9Q*3*>m;G zR#T(8dq+2-zTFjYzw_$hSSMHYlci1xoJV7;;5gp3c+O*-vx|m3b~~jxNk#p!P;`4J z>E|JzXWtqtdHsjqJ7=-!g~!%v`mq0vf6ohQp2zB(M87pJuDWj4$?J*8 z1hzk4p}wn8`sXLlh^4#JH|rgqt)!4NLr2MF6?JdEa&YHL6 zmq@x(Zj@P_$<_FMRI{q?%!(OFypHRW$lu#PB?Y?mSvfZwb@dZHukCLYbjQ|U z$DzD3$;Dk+x5ycba>u`?MGD zi%m*uyTNDl68!tIdk+ngjZ5Ud6WEsw*PpGR2bO4faSoYgjPg0{rQklg_*^H~EZ@F8 zP(=go_N$4*|1Q)ET{fsVPs~}o=g!AdI}o|geo<0xll;ReBh`GJYA3Si{v|Mp&Ljog z?{_hg^i%%rm^C$t^LMqCULP zJA^MyVC$si2Asbi_8ifUNaS_-K_%^(+}L+4>KTU5cUIDgpqxpiJ_#g~2J}y@O`_t} zUxpP_B+}h;L-)EiQ|3d+b)4>h zb|-KyM>=eFRA7ByKf5(zv6^oDN}S*Sk@y@Qsi1G%P|!}Q;B~~Z7jX|jE(!Pp;>B@mYFIIrzME%M#=AU?2n;Y_K`nAah`Rs z`|p!uBAqU~?|5NsDmj(M)pV;*qG>MAZrqrwqN8Oq1`mF%B>DM)Ar}4$YO%<%&Tn=M zzhD0((zm};=FYl@^|^X=?eIIOzrWAy66BUholE7et_l_R>q`^Q`8E9f%WR<}*Fkf3 zHrtBNJ=!JYiKl|!@1@v3|6T?BdW^opD5!E&2PO5~-8b(-rIH4Pe2|t|i$BM27i~IY zHmP_A>NFw$?m|4BYVtbV4fWd3hGElMG*Xg%VSQX$M8`^lK6R(kVK1gQ_mbem`GdKg%qytp2%@4X(F$Gw)1+^0`&mO z0u2rGB)ZdDZ=EH6pXdJf*HUjJalhM5=&Qcodl`;;Z1kFYJ3FCXXL*kjzRr#rC6Jj- z{EWlkTaDV?;$_Ny6|MRBdDEUeu^yKsk>uq~rHwY8Pq+2sB`ZviE37X{l9X?)2|sy`W3h&rq7JMyl&4)q9@z0f38OT zDd5fWWCcYoK6lXihl<`jPN}y*y|Nv_I4<}+-R7ziub^HpG2E|Q{#`|>t)`6a>8+%{ zYs1^Us!QPa;!50qC;(85@pTpu&Ha*!6+G|kq=M&h;k*#|E%zp&zY*r_+Y|Wv@{Q;3 z|7|=K=Ik_B9FRyC-i=@QKugW*G-m~Ajy!Cq-$B9aSKPNPpHzPNwGs7r?f92>KdR{D zp!EU$3KA%*Jf?fEg}_&gTNyo5#8B9C-Nha;yQtxX*Skkk)cp4tsi~v>t*UvcYMOBP zQ~qT4IGWX^nep4VD%xtgTB2T{pmL+KL2EWdb3EndE?SoydNfB@NzF3ChM8hLt{<|^ zebaWlSIo)u*2J;r5TTz)gUVNIIFPBN0Y#U(SL-HHLDay|BP;NHcCW~)!u?|A=GlH) zFL50!)ik;3q64n$)HFnH>lWymM0#`n)0_SpbyffPF9(wp)a^&LNy-ipKS6&pc-rZX zIiX7G_pH%w-EcLv(OdUtae4w>dNXL2G(`13qOLoh>+XGfQc5T#N>hnwpwb}aG^8Od zB~ocrjR!U(uTq?I4(FZiQzI3D?YgpD`C+dDY>*}{;}rKa^lmp+<%Uqg5P^NNlCgV^{+_P5%sbf z@=)$#$9nKE4imdh-qcyn@_q6&9`~b(lJj}s(`Y{L9l`T}+95f~{o|N$-cHJ%<6tWC z7WJhYDDZKll7#1t$Qw0SLEO`Kthm`q&hqf5Gl`s+=qM*=&-kC%9*KC`#H5*NzLZ3M zbJwg|fxL9ZY~vB&GsX+q$Vt-F)(>W#QV_ZJt=iwGg5lEU(c-seqi_2-HIU&ItlYT9EfCfcjqz* zF=)6`{Y+g>yvk>ebh4I`H2sDUIPTqF>_fcQuysr5zBfSs%?(#YU+_`&L`=8B}WY`JIi4(D& zY@Z~ep40Oxa6^=xd+HHSd+gZ2LW%S>SQmy+=J2oF6g) zUvRyo+G~)Uc%EMA`4Bo0R)@3{@z+60_BP}t{aztsc*6Ulg87Q-;XEk5L5b810B983M4kgp`B*gDQ{%k?ix- zYLO4J?G|+ePNko()`%J0_l3u)K0O}?- zmpDT~+9ZbiI4qTsYR$GU?!7%iBt7r{STQD&m^A;qyzNIR3EXi|KOXfC^I5<-Wcc}5 z6t7o`5KpKsd5?+!wrPJryrw@d`Q^0RgBl4mL(zA4@^%=OXBNklqDrQcZXYB z-Ug1LJU#O6{EmALcQ`30FaO=rtd&d2>7a9ZnR_KfzwFArhR@*7C|`&8$Z!bq;F_qh z?(P#51Y^^W-9LfHpg0NflIe;AWkhSte)h zPf(v4WSO7-9wPP`_7{tHI8VK-q8{t)jOI8Ixb;ax(7Mwu;HTL z_KfCpV@JTFQ2r8ibl~{j(;Ej%NcpKQIhPUl*nLsq=nv382hS#TSurFO=a}Y?;@N^P zcUO@k(?eqVjaBhDHcLe)4fOGxz(bEDBtOsXsh%Q|-yZ{NH}W&{(Lw$0)#;LYq&YrkIQjLinVfi!v1;`)QRMSXC2{EUtBJZ6 zcrSX7&L+f@l+@|9s3+O`Yb__OwbBxn4M6>l4^G{SeM3J7KQ~BmCTyMx^>>p;kFEp9 zJ=)QJOC<1gYtx)tw}AU8?;-^*WZxJ4DmL%@R7NtNZu+Xf5bqs)ZRdmGBxPH)Mqm9% zK4-FDMONdWdm!(|&YNxj$5%$4t0!e=gWvlxzWVG5YxHTN7Lvi>(dTUliN3BO^6FtZ z`Da{oG<<-Z`}f`dU)_!KPJLg1yL;!|AM0BvAqT=j#(e@W!0LTXIm2h2$D%J({x6um z@8^fxg}$x;zil=@A*qXu9NJO#bq?_HtT#TkDTw>*UX>dP5>)fkXcyva)z?wBK27C( z-rYn>{72YL^lXsxdh{{){;5~y<-Egw%Ncw^wg!DQ>N_csk%G=)?z8?A@ikb=d1RcU zP7|seTn@@e;_Y9DTo8{~y@q-!Q9lg}N_9y_Kc#VyTU;jiCF=L7 zf&SQr*_CNFrC7hifBGi8!hY~x_<0QS3FD{1BfL48@?|;dpl3bp%a>T7Z**=#<%tJ! z(yDFj=1}Cb6(_4hhhIS+?>NcAphQZBS~_pei32}D=i^Z~wK$u1=FER8;yB_=!1IOZ z2V#7^#+_?^`H%J(r~5bKiKMjQ{APBC(rCYPo%iQI^2bP1Lqjk0*F0v+8rp^jjEm&;1-cVZDm%+VM)xLQtZigs14 z$DcO^VC=U*LQH3lHU1X>9zyUz`ZvjwR=^u2mNBP0e3SA1d>8QS+yC3ux&!#7XW`ii zHX_cpkdxV;f4VtTNyy~xt{?LorM!<)haHe39MZk};0-Q-Cznrbf4}BOB$+bf==S4f zDpG4S++hUpo#_du{uhBi*?b3hITlaA6V?6Is51Ek9yU4cm-)JAz7J3jv3nb@$w*DM z+V$q>qcHvo^$VLD%8+v0xJO31uS+O2bO(=+*yV3;3pv*-eUfm$P@Jy|SDIHO&sCCG z&)jxb=L3i8A6UEXS~TW59(TO`hV!xK&>y6@e!^MNH%2~SeJRx6XHU-E>e@y|uKNG2 znszXX+`UlrxK2w>)(zTlTY-3r>+8oJT8DF-=ys$E`||UWbM1cjt*C~wvBPU z8F0pzTUASRRpi|9eMObne-tmNVL!9J?p7JOeQeL*Li96g23m9;in!6B{_eu0S#mP4 zgK^vV>!_F4=&p4^9m$@{S4IYn(f{kT6#d$Z-E(K3k&}35qIVAUH_byOa~1r#kq?0M z4w_xYdh`!{colt%bN$a4S6@P&w)HEJEyVeyydI6O$(Q3+8YoEs8YzRo%d-8rjrX65 zu@3ay-rN?&_a*9$epQb=EdEK!9-EO$AKhp^pI9IzvSR~sN_xvVFXV)}u(70YE$~5V z@`KfWt-%99Ub{CFxbWN+qWMMi9hND$9_Kyy;?>&vlH;yiazlj!L(Vz7Kt?T8yu0?2WQzU)6D{0rKHk)^}c=B&#uN-9zM(vc_jk*uF|xB3*dvgRo0F_Ridu;lySWa_DOS6 zQhdrBKX2T=Z(dnZqbQVCqjcuSl= z#-p~3B3B-@_|u3uY__HQ+majDuce=kRf8w0nf5WPJI)EiVbD>G+`j%>CF(iG7XoKB zQLd@!jQrFPURQGY4B`i^^Ixh+%w7BX_s5WTU<}g^pU?P2)FY=hEm||^3y&wB@z}@k z;cru=B+CH+UqUCr&gF8^PwOosMQ!`UJy?r;zhum^ZsXff z;xFoyJx0w7!^JHM#kYF_BRDejN> zM(2d2c#i?HpE^gu*HSzQTsOVN+qQ`Y5?*(it9V_6dg5M>cC*{y9Fuy}NkzATtGf&i zT{Z@Ft#``UE!*TwAMtZ^6#3ZPKXkx4oYSG(^iJ-Rk#T=L%PLLfWJPFdr%C7wvCl`| z8f4zAbnbcy=~rz(KnM7q>5;P|$@hdF`ldSsKJA1!VYy2s)!}`?D_g;@F6#Qb7XQ9s z&QnEIEp#36tcVfc&_G%)YN1>;}CM<=d1GQCA-gtjY#{V)-0(OZ%Du zH`bxgLGglbU+6zHY78#cnyxxoEQb+V6G}@=)z%6S+I|R9m{WioGG>K3s=n({F|%ZcIF!uyH^I_#*sgT8h50 zQ~L!D|AC*P?>$xE0n`!9_c$|}_c>8VGaMN$<$aCC(5Z|sUUwdK4?WM*pgW-ZBXF_c z3ns`I?n|nWa{m-NIq$b3pRhUtd4bK7`pJpkTvc65HSoL-OZ^{XKQnxLQtay~h{OF{ z?SCS#C?cyyTg(BU>wkLM1T*kB=@yOI=YS_8b-Zumx#`bWLw(m52j6xUc;w$#%k#lQ z*B;Is9}b?8;olR`5#eIpFNlv_fA8F$h&&RiSl2%a^#_}O>I~dR{jx6d{T>Lun#$Za z;(RJ?dZwMl{&`ouX7eKPebtsK$at>TBmK$oZ5ld7XfluDJ~KHgraP zjYPCSEt>3{mb!W}@@>IO!xe+DKdsWQ+Nk7G^7mp`mp;(jFKFA91-(;=S?(3lQ zpgIVAf69ybqn==X%~us<0*tIJ8{|aeN53^4TcDmufX+p7--K)>=+HqH=vd1ANpBY%I$NGz_MoMeUm z(a(=>Mh;XkKIEZZG{=kn(Bo8Gnf)vSe3NO}6SL#!qtbQX2D}u~_R3)NIbI%qVR=|8 zCtr($%RMX+C+S@9Tj<14XxyRkg!&gIgO4v?x#`mO;$d4CnS zoaGzz?Jr)O9i|SRll4!o%gM1DqdTm;1HLn?>z;&(s7DuEj*my4Oa3bVdZrF_P!KH*zxXb4J}a)F;~| z$(~x+SC>Ll9Cjd2F#bIPeS7_S& afVOhdckA?7mHmLPo4%d6 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

-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 z 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%0DLF-W0QvXTR$cOKaWg-|3u$+8LyW05%jv? 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�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±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 QGvo6UGqbo=ImPJz0Duc{@&Et; literal 0 HcmV?d00001 From 67e3ec45822572f147f785999dfa0c4121d01635 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Thu, 2 Oct 2025 07:05:10 -0400 Subject: [PATCH 083/102] feat: add spac arcsinh_norm interactive_spatial_plot galaxy tools --- .../run_spac_template.sh | 391 ++++++++++++++++++ .../spac_arcsinh_normalization.xml | 47 +++ .../run_spac_template.sh | 391 ++++++++++++++++++ .../spac_interactive_spatial_plot.xml | 92 +++++ 4 files changed, 921 insertions(+) create mode 100644 galaxy_tools/spac_arcsinh_normalization/run_spac_template.sh create mode 100644 galaxy_tools/spac_arcsinh_normalization/spac_arcsinh_normalization.xml create mode 100644 galaxy_tools/spac_interactive_spatial_plot/run_spac_template.sh create mode 100644 galaxy_tools/spac_interactive_spatial_plot/spac_interactive_spatial_plot.xml diff --git a/galaxy_tools/spac_arcsinh_normalization/run_spac_template.sh b/galaxy_tools/spac_arcsinh_normalization/run_spac_template.sh new file mode 100644 index 00000000..f2c32068 --- /dev/null +++ b/galaxy_tools/spac_arcsinh_normalization/run_spac_template.sh @@ -0,0 +1,391 @@ +#!/usr/bin/env bash +# run_spac_template.sh - Docker version for Galaxy +# Version: 4.0.0 - Imports templates from installed SPAC package +set -euo pipefail + +# Log everything to tool_stdout.txt +exec > >(tee -a tool_stdout.txt) 2>&1 + +PARAMS_JSON="${1:?Missing params.json path}" +TEMPLATE_NAME="${2:?Missing template name}" # Just the name, not filename + +# Use system Python inside Docker container +SPAC_PYTHON="${SPAC_PYTHON:-python3}" + +echo "=== SPAC Template Wrapper v4.0 (Docker) ===" +echo "Parameters: $PARAMS_JSON" +echo "Template: $TEMPLATE_NAME" +echo "Python: $SPAC_PYTHON" +echo "Working directory: $(pwd)" + +# Run template through Python interpreter +"$SPAC_PYTHON" - <<'PYTHON_RUNNER' "$PARAMS_JSON" "$TEMPLATE_NAME" +import json +import os +import sys +import copy +import importlib +import traceback +import inspect + +# Get command line arguments +params_path = sys.argv[1] +template_name = sys.argv[2] # Just the name like "boxplot", not "boxplot_template.py" + +print(f"[Runner] Starting execution for template: {template_name}") +print(f"[Runner] Python version: {sys.version}") + +# ============================================================================ +# HELPER FUNCTIONS +# ============================================================================ + +def determine_outputs_from_params(params): + """Read outputs from params if available, otherwise use defaults""" + if 'outputs' in params and isinstance(params['outputs'], dict): + outputs = params['outputs'] + print(f"[Runner] Using outputs from params: {list(outputs.keys())}") + return outputs + + # Fallback to defaults based on template name + print(f"[Runner] No outputs in params, using defaults for {template_name}") + + if template_name in ['boxplot', 'histogram', 'violin_plot', 'scatter_plot', + 'hierarchical_heatmap', 'relational_heatmap']: + return {'figures': 'figure_folder', 'DataFrames': 'dataframe_folder'} + elif template_name in ['interactive_spatial_plot', 'interactive_scatter_plot']: + return {'html': 'html_folder'} + elif template_name in ['analysis_to_csv', 'select_values']: + return {'DataFrames': 'dataframe_folder'} + elif template_name == 'setup_analysis': + return {'analysis': 'analysis_output.pickle'} + else: + return {'analysis': 'transform_output.pickle'} + +def inject_output_paths(params, outputs, template_name): + """Add output paths to parameters""" + params_exec = copy.deepcopy(params) + + # Remove the 'outputs' field before execution + params_exec.pop('outputs', None) + + params_exec['save_results'] = True + + if 'analysis' in outputs: + params_exec['output_path'] = outputs['analysis'] + params_exec['Output_Path'] = outputs['analysis'] + params_exec['Output_File'] = outputs['analysis'] + + if 'DataFrames' in outputs: + df_dir = outputs['DataFrames'] + params_exec['output_dir'] = df_dir + params_exec['Export_Dir'] = df_dir + params_exec['Output_File'] = os.path.join(df_dir, f'{template_name}_output.csv') + + if 'figures' in outputs: + fig_dir = outputs['figures'] + params_exec['figure_dir'] = fig_dir + params_exec['Figure_Dir'] = fig_dir + params_exec['Figure_File'] = os.path.join(fig_dir, f'{template_name}.png') + + if 'html' in outputs: + html_dir = outputs['html'] + params_exec['output_path'] = os.path.join(html_dir, f'{template_name}.html') + params_exec['Output_File'] = params_exec['output_path'] + + return params_exec + +def handle_template_results(result, outputs, template_name): + """Save any in-memory results returned by the template""" + if result is None: + return + + saved_count = 0 + + try: + import pandas as pd + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + + if isinstance(result, tuple): + for i, item in enumerate(result): + if hasattr(item, 'savefig') and 'figures' in outputs: + fig_path = os.path.join(outputs['figures'], f'{template_name}_{i+1}.png') + item.savefig(fig_path, dpi=300, bbox_inches='tight') + print(f"[Runner] Saved figure to {fig_path}") + plt.close(item) + saved_count += 1 + elif hasattr(item, 'to_csv') and 'DataFrames' in outputs: + csv_path = os.path.join(outputs['DataFrames'], f'{template_name}_{i+1}.csv') + item.to_csv(csv_path, index=True) + print(f"[Runner] Saved DataFrame to {csv_path}") + saved_count += 1 + + elif hasattr(result, 'to_csv') and 'DataFrames' in outputs: + csv_path = os.path.join(outputs['DataFrames'], f'{template_name}_output.csv') + result.to_csv(csv_path, index=True) + print(f"[Runner] Saved DataFrame to {csv_path}") + saved_count += 1 + + elif hasattr(result, 'savefig') and 'figures' in outputs: + fig_path = os.path.join(outputs['figures'], f'{template_name}.png') + result.savefig(fig_path, dpi=300, bbox_inches='tight') + print(f"[Runner] Saved figure to {fig_path}") + plt.close(result) + saved_count += 1 + + elif hasattr(result, 'write_h5ad') and 'analysis' in outputs: + result.write_h5ad(outputs['analysis']) + print(f"[Runner] Saved AnnData to {outputs['analysis']}") + saved_count += 1 + + except ImportError as e: + print(f"[Runner] Note: Some libraries not available for result handling: {e}") + + if saved_count > 0: + print(f"[Runner] Saved {saved_count} in-memory results") + +def verify_outputs(outputs): + """Verify that expected outputs were created""" + found_outputs = False + + for output_type, path in outputs.items(): + if output_type == 'analysis': + if os.path.exists(path): + size = os.path.getsize(path) + print(f"[Runner] ✓ {output_type}: {os.path.basename(path)} ({size:,} bytes)") + found_outputs = True + else: + print(f"[Runner] ✗ {output_type}: NOT FOUND at {path}") + else: + if os.path.exists(path) and os.path.isdir(path): + files = os.listdir(path) + if files: + total_size = sum(os.path.getsize(os.path.join(path, f)) for f in files) + print(f"[Runner] ✓ {output_type}: {len(files)} files ({total_size:,} bytes)") + for f in files[:3]: + size = os.path.getsize(os.path.join(path, f)) + print(f"[Runner] - {f} ({size:,} bytes)") + if len(files) > 3: + print(f"[Runner] ... and {len(files)-3} more files") + found_outputs = True + else: + print(f"[Runner] ⚠ {output_type}: directory exists but empty") + + if not found_outputs: + print("[Runner] WARNING: No outputs were created!") + +# ============================================================================ +# PARAMETER PROCESSING +# ============================================================================ + +def _unsanitize(s: str) -> str: + """Remove Galaxy's parameter sanitization tokens""" + replacements = { + '__ob__': '[', '__cb__': ']', + '__oc__': '{', '__cc__': '}', + '__dq__': '"', '__sq__': "'", + '__gt__': '>', '__lt__': '<', + '__cn__': '\n', '__cr__': '\r', + '__tc__': '\t', '__pd__': '#', + '__at__': '@', '__cm__': ',', + '__dollar__': '$', '__us__': '_' + } + for token, char in replacements.items(): + s = s.replace(token, char) + return s + +def _maybe_parse(v): + """Recursively unsanitize and parse JSON where possible""" + if isinstance(v, str): + u = _unsanitize(v).strip() + if (u.startswith('[') and u.endswith(']')) or (u.startswith('{') and u.endswith('}')): + try: + return json.loads(u) + except: + return u + return u + elif isinstance(v, dict): + return {k: _maybe_parse(val) for k, val in v.items()} + elif isinstance(v, list): + return [_maybe_parse(item) for item in v] + return v + +def should_normalize_as_list(key, value): + """Determine if a parameter should be normalized as a list""" + if isinstance(value, list): + return True + + if value is None or value == "": + return False + + key_lower = key.lower() + + if 'regex' in key_lower or 'pattern' in key_lower: + return False + + if any(x in key_lower for x in ['single', 'one', 'first', 'second', 'primary']): + return False + + if any(x in key_lower for x in ['features', 'markers', 'phenotypes', 'annotations', + 'columns', 'types', 'labels', 'regions', 'radii']): + return True + + if isinstance(value, str): + if ',' in value or '\n' in value: + return True + if value.strip().startswith('[') and value.strip().endswith(']'): + return True + + return False + +def normalize_to_list(value): + """Convert various input formats to a proper Python list""" + if value in (None, "", "All", ["All"], "all", ["all"]): + return ["All"] + + if isinstance(value, list): + return value + + if isinstance(value, str): + s = value.strip() + + if s.startswith('[') and s.endswith(']'): + try: + parsed = json.loads(s) + return parsed if isinstance(parsed, list) else [str(parsed)] + except: + pass + + if ',' in s: + return [item.strip() for item in s.split(',') if item.strip()] + + if '\n' in s: + return [item.strip() for item in s.split('\n') if item.strip()] + + return [s] if s else [] + + return [value] if value is not None else [] + +# ============================================================================ +# MAIN EXECUTION +# ============================================================================ + +# Load parameters +with open(params_path, 'r') as f: + params = json.load(f) + print(f"[Runner] Loaded {len(params)} parameters from {params_path}") + +# Step 1: De-sanitize Galaxy parameters +print("[Runner] Step 1: De-sanitizing Galaxy parameters") +params = _maybe_parse(params) + +# Step 2: Get outputs from params (injected by Galaxy) +print("[Runner] Step 2: Getting output structure") +outputs = determine_outputs_from_params(params) + +# Create output directories +for output_type, path in outputs.items(): + if output_type != 'analysis' and path: + os.makedirs(path, exist_ok=True) + print(f"[Runner] Created {output_type} directory: {path}") + +# Step 3: Normalize list parameters +print("[Runner] Step 3: Normalizing list parameters") +list_count = 0 +for key, value in list(params.items()): + if key != 'outputs' and should_normalize_as_list(key, value): + original = value + normalized = normalize_to_list(value) + if original != normalized: + params[key] = normalized + list_count += 1 + print(f"[Runner] Normalized {key}: {original} -> {normalized}") + +if list_count > 0: + print(f"[Runner] Normalized {list_count} list parameters") + +# Step 4: Inject output paths +print("[Runner] Step 4: Injecting output paths") +params_exec = inject_output_paths(params, outputs, template_name) + +# Save parameter files +with open('params.exec.json', 'w') as f: + json.dump(params_exec, f, indent=2) + +with open('config_used.json', 'w') as f: + params_display = {k: v for k, v in params.items() + if k != 'outputs' and + not any(x in k.lower() for x in ['output', 'save', 'path', 'dir', 'file', 'export'])} + json.dump(params_display, f, indent=2) + +print(f"[Runner] Saved params.exec.json ({len(params_exec)} parameters)") +print(f"[Runner] Saved config_used.json ({len(params_display)} display parameters)") + +# Step 5: Load template module from installed SPAC package +print(f"[Runner] Step 5: Loading template '{template_name}' from SPAC package") + +try: + # Import from spac.templates package + module_name = f'spac.templates.{template_name}_template' + mod = importlib.import_module(module_name) + print(f"[Runner] Successfully imported {module_name}") +except ImportError as e: + print(f"[Runner] ERROR: Could not import {module_name}: {e}") + print(f"[Runner] Available modules in spac.templates:") + try: + import spac.templates + import pkgutil + for importer, modname, ispkg in pkgutil.iter_modules(spac.templates.__path__): + print(f"[Runner] - {modname}") + except: + pass + sys.exit(1) + +if not hasattr(mod, 'run_from_json'): + print('[Runner] ERROR: Template missing run_from_json function') + sys.exit(2) + +# Step 6: Execute template +print("[Runner] Step 6: Executing template") +sig = inspect.signature(mod.run_from_json) +kwargs = {} + +if 'save_results' in sig.parameters: + kwargs['save_results'] = True + print("[Runner] Added save_results=True to kwargs") + +if 'show_plot' in sig.parameters: + kwargs['show_plot'] = False + print("[Runner] Added show_plot=False to kwargs") + +try: + print(f"[Runner] Calling run_from_json('params.exec.json', **{kwargs})") + result = mod.run_from_json('params.exec.json', **kwargs) + print(f"[Runner] Template completed successfully, returned {type(result).__name__}") +except Exception as e: + print(f"[Runner] ERROR in template execution: {e}") + print(f"[Runner] Error type: {type(e).__name__}") + traceback.print_exc() + sys.exit(1) + +# Handle any returned objects +handle_template_results(result, outputs, template_name) + +# Step 7: Verify outputs +print("[Runner] Step 7: Verifying outputs") +verify_outputs(outputs) + +print("[Runner] === Execution completed successfully ===") + +PYTHON_RUNNER + +EXIT_CODE=$? + +if [ $EXIT_CODE -ne 0 ]; then + echo "" + echo "=== Template execution failed with exit code $EXIT_CODE ===" + echo "" +fi + +exit $EXIT_CODE \ No newline at end of file diff --git a/galaxy_tools/spac_arcsinh_normalization/spac_arcsinh_normalization.xml b/galaxy_tools/spac_arcsinh_normalization/spac_arcsinh_normalization.xml new file mode 100644 index 00000000..738a7432 --- /dev/null +++ b/galaxy_tools/spac_arcsinh_normalization/spac_arcsinh_normalization.xml @@ -0,0 +1,47 @@ + + Normalize features either by a user-defined co-factor or a determined percentile. + + + nciccbr/spac:v1 + + + + python3 + + + tool_stdout.txt && + + ## Run the universal wrapper + bash $__tool_directory__/run_spac_template.sh "$params_json" arcsinh_normalization + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/galaxy_tools/spac_interactive_spatial_plot/run_spac_template.sh b/galaxy_tools/spac_interactive_spatial_plot/run_spac_template.sh new file mode 100644 index 00000000..f2c32068 --- /dev/null +++ b/galaxy_tools/spac_interactive_spatial_plot/run_spac_template.sh @@ -0,0 +1,391 @@ +#!/usr/bin/env bash +# run_spac_template.sh - Docker version for Galaxy +# Version: 4.0.0 - Imports templates from installed SPAC package +set -euo pipefail + +# Log everything to tool_stdout.txt +exec > >(tee -a tool_stdout.txt) 2>&1 + +PARAMS_JSON="${1:?Missing params.json path}" +TEMPLATE_NAME="${2:?Missing template name}" # Just the name, not filename + +# Use system Python inside Docker container +SPAC_PYTHON="${SPAC_PYTHON:-python3}" + +echo "=== SPAC Template Wrapper v4.0 (Docker) ===" +echo "Parameters: $PARAMS_JSON" +echo "Template: $TEMPLATE_NAME" +echo "Python: $SPAC_PYTHON" +echo "Working directory: $(pwd)" + +# Run template through Python interpreter +"$SPAC_PYTHON" - <<'PYTHON_RUNNER' "$PARAMS_JSON" "$TEMPLATE_NAME" +import json +import os +import sys +import copy +import importlib +import traceback +import inspect + +# Get command line arguments +params_path = sys.argv[1] +template_name = sys.argv[2] # Just the name like "boxplot", not "boxplot_template.py" + +print(f"[Runner] Starting execution for template: {template_name}") +print(f"[Runner] Python version: {sys.version}") + +# ============================================================================ +# HELPER FUNCTIONS +# ============================================================================ + +def determine_outputs_from_params(params): + """Read outputs from params if available, otherwise use defaults""" + if 'outputs' in params and isinstance(params['outputs'], dict): + outputs = params['outputs'] + print(f"[Runner] Using outputs from params: {list(outputs.keys())}") + return outputs + + # Fallback to defaults based on template name + print(f"[Runner] No outputs in params, using defaults for {template_name}") + + if template_name in ['boxplot', 'histogram', 'violin_plot', 'scatter_plot', + 'hierarchical_heatmap', 'relational_heatmap']: + return {'figures': 'figure_folder', 'DataFrames': 'dataframe_folder'} + elif template_name in ['interactive_spatial_plot', 'interactive_scatter_plot']: + return {'html': 'html_folder'} + elif template_name in ['analysis_to_csv', 'select_values']: + return {'DataFrames': 'dataframe_folder'} + elif template_name == 'setup_analysis': + return {'analysis': 'analysis_output.pickle'} + else: + return {'analysis': 'transform_output.pickle'} + +def inject_output_paths(params, outputs, template_name): + """Add output paths to parameters""" + params_exec = copy.deepcopy(params) + + # Remove the 'outputs' field before execution + params_exec.pop('outputs', None) + + params_exec['save_results'] = True + + if 'analysis' in outputs: + params_exec['output_path'] = outputs['analysis'] + params_exec['Output_Path'] = outputs['analysis'] + params_exec['Output_File'] = outputs['analysis'] + + if 'DataFrames' in outputs: + df_dir = outputs['DataFrames'] + params_exec['output_dir'] = df_dir + params_exec['Export_Dir'] = df_dir + params_exec['Output_File'] = os.path.join(df_dir, f'{template_name}_output.csv') + + if 'figures' in outputs: + fig_dir = outputs['figures'] + params_exec['figure_dir'] = fig_dir + params_exec['Figure_Dir'] = fig_dir + params_exec['Figure_File'] = os.path.join(fig_dir, f'{template_name}.png') + + if 'html' in outputs: + html_dir = outputs['html'] + params_exec['output_path'] = os.path.join(html_dir, f'{template_name}.html') + params_exec['Output_File'] = params_exec['output_path'] + + return params_exec + +def handle_template_results(result, outputs, template_name): + """Save any in-memory results returned by the template""" + if result is None: + return + + saved_count = 0 + + try: + import pandas as pd + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + + if isinstance(result, tuple): + for i, item in enumerate(result): + if hasattr(item, 'savefig') and 'figures' in outputs: + fig_path = os.path.join(outputs['figures'], f'{template_name}_{i+1}.png') + item.savefig(fig_path, dpi=300, bbox_inches='tight') + print(f"[Runner] Saved figure to {fig_path}") + plt.close(item) + saved_count += 1 + elif hasattr(item, 'to_csv') and 'DataFrames' in outputs: + csv_path = os.path.join(outputs['DataFrames'], f'{template_name}_{i+1}.csv') + item.to_csv(csv_path, index=True) + print(f"[Runner] Saved DataFrame to {csv_path}") + saved_count += 1 + + elif hasattr(result, 'to_csv') and 'DataFrames' in outputs: + csv_path = os.path.join(outputs['DataFrames'], f'{template_name}_output.csv') + result.to_csv(csv_path, index=True) + print(f"[Runner] Saved DataFrame to {csv_path}") + saved_count += 1 + + elif hasattr(result, 'savefig') and 'figures' in outputs: + fig_path = os.path.join(outputs['figures'], f'{template_name}.png') + result.savefig(fig_path, dpi=300, bbox_inches='tight') + print(f"[Runner] Saved figure to {fig_path}") + plt.close(result) + saved_count += 1 + + elif hasattr(result, 'write_h5ad') and 'analysis' in outputs: + result.write_h5ad(outputs['analysis']) + print(f"[Runner] Saved AnnData to {outputs['analysis']}") + saved_count += 1 + + except ImportError as e: + print(f"[Runner] Note: Some libraries not available for result handling: {e}") + + if saved_count > 0: + print(f"[Runner] Saved {saved_count} in-memory results") + +def verify_outputs(outputs): + """Verify that expected outputs were created""" + found_outputs = False + + for output_type, path in outputs.items(): + if output_type == 'analysis': + if os.path.exists(path): + size = os.path.getsize(path) + print(f"[Runner] ✓ {output_type}: {os.path.basename(path)} ({size:,} bytes)") + found_outputs = True + else: + print(f"[Runner] ✗ {output_type}: NOT FOUND at {path}") + else: + if os.path.exists(path) and os.path.isdir(path): + files = os.listdir(path) + if files: + total_size = sum(os.path.getsize(os.path.join(path, f)) for f in files) + print(f"[Runner] ✓ {output_type}: {len(files)} files ({total_size:,} bytes)") + for f in files[:3]: + size = os.path.getsize(os.path.join(path, f)) + print(f"[Runner] - {f} ({size:,} bytes)") + if len(files) > 3: + print(f"[Runner] ... and {len(files)-3} more files") + found_outputs = True + else: + print(f"[Runner] ⚠ {output_type}: directory exists but empty") + + if not found_outputs: + print("[Runner] WARNING: No outputs were created!") + +# ============================================================================ +# PARAMETER PROCESSING +# ============================================================================ + +def _unsanitize(s: str) -> str: + """Remove Galaxy's parameter sanitization tokens""" + replacements = { + '__ob__': '[', '__cb__': ']', + '__oc__': '{', '__cc__': '}', + '__dq__': '"', '__sq__': "'", + '__gt__': '>', '__lt__': '<', + '__cn__': '\n', '__cr__': '\r', + '__tc__': '\t', '__pd__': '#', + '__at__': '@', '__cm__': ',', + '__dollar__': '$', '__us__': '_' + } + for token, char in replacements.items(): + s = s.replace(token, char) + return s + +def _maybe_parse(v): + """Recursively unsanitize and parse JSON where possible""" + if isinstance(v, str): + u = _unsanitize(v).strip() + if (u.startswith('[') and u.endswith(']')) or (u.startswith('{') and u.endswith('}')): + try: + return json.loads(u) + except: + return u + return u + elif isinstance(v, dict): + return {k: _maybe_parse(val) for k, val in v.items()} + elif isinstance(v, list): + return [_maybe_parse(item) for item in v] + return v + +def should_normalize_as_list(key, value): + """Determine if a parameter should be normalized as a list""" + if isinstance(value, list): + return True + + if value is None or value == "": + return False + + key_lower = key.lower() + + if 'regex' in key_lower or 'pattern' in key_lower: + return False + + if any(x in key_lower for x in ['single', 'one', 'first', 'second', 'primary']): + return False + + if any(x in key_lower for x in ['features', 'markers', 'phenotypes', 'annotations', + 'columns', 'types', 'labels', 'regions', 'radii']): + return True + + if isinstance(value, str): + if ',' in value or '\n' in value: + return True + if value.strip().startswith('[') and value.strip().endswith(']'): + return True + + return False + +def normalize_to_list(value): + """Convert various input formats to a proper Python list""" + if value in (None, "", "All", ["All"], "all", ["all"]): + return ["All"] + + if isinstance(value, list): + return value + + if isinstance(value, str): + s = value.strip() + + if s.startswith('[') and s.endswith(']'): + try: + parsed = json.loads(s) + return parsed if isinstance(parsed, list) else [str(parsed)] + except: + pass + + if ',' in s: + return [item.strip() for item in s.split(',') if item.strip()] + + if '\n' in s: + return [item.strip() for item in s.split('\n') if item.strip()] + + return [s] if s else [] + + return [value] if value is not None else [] + +# ============================================================================ +# MAIN EXECUTION +# ============================================================================ + +# Load parameters +with open(params_path, 'r') as f: + params = json.load(f) + print(f"[Runner] Loaded {len(params)} parameters from {params_path}") + +# Step 1: De-sanitize Galaxy parameters +print("[Runner] Step 1: De-sanitizing Galaxy parameters") +params = _maybe_parse(params) + +# Step 2: Get outputs from params (injected by Galaxy) +print("[Runner] Step 2: Getting output structure") +outputs = determine_outputs_from_params(params) + +# Create output directories +for output_type, path in outputs.items(): + if output_type != 'analysis' and path: + os.makedirs(path, exist_ok=True) + print(f"[Runner] Created {output_type} directory: {path}") + +# Step 3: Normalize list parameters +print("[Runner] Step 3: Normalizing list parameters") +list_count = 0 +for key, value in list(params.items()): + if key != 'outputs' and should_normalize_as_list(key, value): + original = value + normalized = normalize_to_list(value) + if original != normalized: + params[key] = normalized + list_count += 1 + print(f"[Runner] Normalized {key}: {original} -> {normalized}") + +if list_count > 0: + print(f"[Runner] Normalized {list_count} list parameters") + +# Step 4: Inject output paths +print("[Runner] Step 4: Injecting output paths") +params_exec = inject_output_paths(params, outputs, template_name) + +# Save parameter files +with open('params.exec.json', 'w') as f: + json.dump(params_exec, f, indent=2) + +with open('config_used.json', 'w') as f: + params_display = {k: v for k, v in params.items() + if k != 'outputs' and + not any(x in k.lower() for x in ['output', 'save', 'path', 'dir', 'file', 'export'])} + json.dump(params_display, f, indent=2) + +print(f"[Runner] Saved params.exec.json ({len(params_exec)} parameters)") +print(f"[Runner] Saved config_used.json ({len(params_display)} display parameters)") + +# Step 5: Load template module from installed SPAC package +print(f"[Runner] Step 5: Loading template '{template_name}' from SPAC package") + +try: + # Import from spac.templates package + module_name = f'spac.templates.{template_name}_template' + mod = importlib.import_module(module_name) + print(f"[Runner] Successfully imported {module_name}") +except ImportError as e: + print(f"[Runner] ERROR: Could not import {module_name}: {e}") + print(f"[Runner] Available modules in spac.templates:") + try: + import spac.templates + import pkgutil + for importer, modname, ispkg in pkgutil.iter_modules(spac.templates.__path__): + print(f"[Runner] - {modname}") + except: + pass + sys.exit(1) + +if not hasattr(mod, 'run_from_json'): + print('[Runner] ERROR: Template missing run_from_json function') + sys.exit(2) + +# Step 6: Execute template +print("[Runner] Step 6: Executing template") +sig = inspect.signature(mod.run_from_json) +kwargs = {} + +if 'save_results' in sig.parameters: + kwargs['save_results'] = True + print("[Runner] Added save_results=True to kwargs") + +if 'show_plot' in sig.parameters: + kwargs['show_plot'] = False + print("[Runner] Added show_plot=False to kwargs") + +try: + print(f"[Runner] Calling run_from_json('params.exec.json', **{kwargs})") + result = mod.run_from_json('params.exec.json', **kwargs) + print(f"[Runner] Template completed successfully, returned {type(result).__name__}") +except Exception as e: + print(f"[Runner] ERROR in template execution: {e}") + print(f"[Runner] Error type: {type(e).__name__}") + traceback.print_exc() + sys.exit(1) + +# Handle any returned objects +handle_template_results(result, outputs, template_name) + +# Step 7: Verify outputs +print("[Runner] Step 7: Verifying outputs") +verify_outputs(outputs) + +print("[Runner] === Execution completed successfully ===") + +PYTHON_RUNNER + +EXIT_CODE=$? + +if [ $EXIT_CODE -ne 0 ]; then + echo "" + echo "=== Template execution failed with exit code $EXIT_CODE ===" + echo "" +fi + +exit $EXIT_CODE \ No newline at end of file diff --git a/galaxy_tools/spac_interactive_spatial_plot/spac_interactive_spatial_plot.xml b/galaxy_tools/spac_interactive_spatial_plot/spac_interactive_spatial_plot.xml new file mode 100644 index 00000000..68b49576 --- /dev/null +++ b/galaxy_tools/spac_interactive_spatial_plot/spac_interactive_spatial_plot.xml @@ -0,0 +1,92 @@ + + Creates interactive, real-time visual explorations of single-cell spatial data for in-depth analysis. + + + nciccbr/spac:v1 + + + + python3 + + + tool_stdout.txt && + + ## Run the universal wrapper + bash $__tool_directory__/run_spac_template.sh "$params_json" interactive_spatial_plot + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@misc{spac_toolkit, + author = {FNLCR DMAP Team}, + title = {SPAC: SPAtial single-Cell analysis}, + year = {2023}, + url = {https://github.com/FNLCR-DMAP/SCSAWorkflow} +} + + + \ No newline at end of file From d2526a79965acb886c0100a04385f5325ec1923b Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Thu, 2 Oct 2025 12:35:32 -0400 Subject: [PATCH 084/102] feat: add spac_load_csv_files galaxy tools --- .../spac_load_csv_files/run_spac_template.sh | 403 ++++++++++++++++++ .../spac_load_csv_files.xml | 84 ++++ 2 files changed, 487 insertions(+) create mode 100644 galaxy_tools/spac_load_csv_files/run_spac_template.sh create mode 100644 galaxy_tools/spac_load_csv_files/spac_load_csv_files.xml diff --git a/galaxy_tools/spac_load_csv_files/run_spac_template.sh b/galaxy_tools/spac_load_csv_files/run_spac_template.sh new file mode 100644 index 00000000..a67c68eb --- /dev/null +++ b/galaxy_tools/spac_load_csv_files/run_spac_template.sh @@ -0,0 +1,403 @@ +#!/usr/bin/env bash +# run_spac_template.sh - SPAC wrapper with Load CSV fixes +# Version: 5.3.0 - Complete version with Load CSV handling +set -euo pipefail + +PARAMS_JSON="${1:?Missing params.json path}" +TEMPLATE_BASE="${2:?Missing template base name}" + +# Handle both base names and full .py filenames for backward compatibility +if [[ "$TEMPLATE_BASE" == *.py ]]; then + TEMPLATE_PY="$TEMPLATE_BASE" +elif [[ "$TEMPLATE_BASE" == "load_csv_files_with_config" ]]; then + TEMPLATE_PY="load_csv_files_with_config.py" +else + TEMPLATE_PY="${TEMPLATE_BASE}_template.py" +fi + +# Use SPAC Python environment +SPAC_PYTHON="${SPAC_PYTHON:-python3}" + +echo "=== SPAC Template Wrapper v5.3 ===" +echo "Parameters: $PARAMS_JSON" +echo "Template base: $TEMPLATE_BASE" +echo "Template file: $TEMPLATE_PY" +echo "Python: $SPAC_PYTHON" + +# Run template through Python +"$SPAC_PYTHON" - <<'PYTHON_RUNNER' "$PARAMS_JSON" "$TEMPLATE_PY" 2>&1 | tee tool_stdout.txt +import json +import os +import sys +import copy +import traceback +import inspect +import shutil + +# Get arguments +params_path = sys.argv[1] +template_filename = sys.argv[2] + +print(f"[Runner] Loading parameters from: {params_path}") +print(f"[Runner] Template: {template_filename}") + +# Load parameters +with open(params_path, 'r') as f: + params = json.load(f) + +# --------------------------------------------------------------------------- +# De-sanitization and parsing helpers +# --------------------------------------------------------------------------- +def _unsanitize(s: str) -> str: + """Remove Galaxy's parameter sanitization tokens""" + if not isinstance(s, str): + return s + replacements = { + '__ob__': '[', '__cb__': ']', + '__oc__': '{', '__cc__': '}', + '__dq__': '"', '__sq__': "'", + '__gt__': '>', '__lt__': '<', + '__cn__': '\n', '__cr__': '\r', + '__tc__': '\t', '__pd__': '#', + '__at__': '@', '__cm__': ',' + } + for token, char in replacements.items(): + s = s.replace(token, char) + return s + +def _maybe_parse(v): + """Recursively de-sanitize and JSON-parse strings where possible.""" + if isinstance(v, str): + u = _unsanitize(v).strip() + if (u.startswith('[') and u.endswith(']')) or (u.startswith('{') and u.endswith('}')): + try: + return json.loads(u) + except Exception: + return u + return u + elif isinstance(v, dict): + return {k: _maybe_parse(val) for k, val in v.items()} + elif isinstance(v, list): + return [_maybe_parse(item) for item in v] + return v + +# First normalize the whole params tree +params = _maybe_parse(params) + +# Extract template name +template_name = os.path.basename(template_filename).replace('_template.py', '').replace('.py', '') + +# Helper function to coerce singleton lists to strings for load_csv +def _coerce_singleton_paths_for_load_csv(params, template_name): + """For load_csv templates, flatten 1-item lists to strings for path-like params.""" + if 'load_csv' not in template_name: + return params + for key in ('CSV_Files', 'CSV_Files_Configuration'): + val = params.get(key) + if isinstance(val, list) and len(val) == 1 and isinstance(val[0], (str, bytes)): + params[key] = val[0] + print(f"[Runner] Coerced {key} from list -> string") + return params + +# Special handling for String_Columns in load_csv templates +if 'load_csv' in template_name and 'String_Columns' in params: + value = params['String_Columns'] + if not isinstance(value, list): + if value in [None, "", "[]", "__ob____cb__"]: + params['String_Columns'] = [] + elif isinstance(value, str): + s = value.strip() + if s.startswith('[') and s.endswith(']'): + try: + params['String_Columns'] = json.loads(s) + except: + params['String_Columns'] = [s] if s else [] + elif ',' in s: + params['String_Columns'] = [item.strip() for item in s.split(',') if item.strip()] + else: + params['String_Columns'] = [s] if s else [] + else: + params['String_Columns'] = [] + print(f"[Runner] Ensured String_Columns is list: {params['String_Columns']}") + +# Apply the coercion for load_csv files +params = _coerce_singleton_paths_for_load_csv(params, template_name) + +# CRITICAL FIX: For Load CSV Files, check if we have csv_input_dir +if 'load_csv' in template_name and 'CSV_Files' in params: + # Check if csv_input_dir was created by Galaxy command + if os.path.exists('csv_input_dir') and os.path.isdir('csv_input_dir'): + params['CSV_Files'] = 'csv_input_dir' + print("[Runner] Using csv_input_dir created by Galaxy") + elif isinstance(params['CSV_Files'], str) and os.path.isfile(params['CSV_Files']): + # We have a single file path, need to get its directory + params['CSV_Files'] = os.path.dirname(params['CSV_Files']) + print(f"[Runner] Using directory of CSV file: {params['CSV_Files']}") + +# Extract outputs specification +raw_outputs = params.pop('outputs', {}) +outputs = {} + +if isinstance(raw_outputs, dict): + outputs = raw_outputs +elif isinstance(raw_outputs, str): + try: + maybe = json.loads(_unsanitize(raw_outputs)) + if isinstance(maybe, dict): + outputs = maybe + except Exception: + pass + +if not isinstance(outputs, dict) or not outputs: + print("[Runner] Warning: 'outputs' missing or not a dict; using defaults") + if 'boxplot' in template_name or 'plot' in template_name or 'histogram' in template_name: + outputs = {'DataFrames': 'dataframe_folder', 'figures': 'figure_folder'} + elif 'load_csv' in template_name: + outputs = {'DataFrames': 'dataframe_folder'} + elif 'interactive' in template_name: + outputs = {'html': 'html_folder'} + else: + outputs = {'analysis': 'transform_output.pickle'} + +print(f"[Runner] Outputs -> {list(outputs.keys())}") + +# Create output directories +for output_type, path in outputs.items(): + if output_type != 'analysis' and path: + os.makedirs(path, exist_ok=True) + print(f"[Runner] Created {output_type} directory: {path}") + +# Normalize list parameters for features +feature_keys = [ + 'Feature_s_to_Plot', 'Features_to_Plot', 'features', + 'Features', 'Phenotypes', 'Markers', 'Regions' +] + +for key in feature_keys: + if key in params: + value = params[key] + if value in (None, "", "All", ["All"], "all", ["all"]): + params[key] = ["All"] + elif isinstance(value, str): + u = _unsanitize(value).strip() + if u.startswith('[') and u.endswith(']'): + try: + params[key] = json.loads(u) + except: + if ',' in u: + params[key] = [s.strip() for s in u.split(',') if s.strip()] + else: + params[key] = [u] if u else [] + elif ',' in u: + params[key] = [s.strip() for s in u.split(',') if s.strip()] + elif '\n' in u: + params[key] = [s.strip() for s in u.split('\n') if s.strip()] + else: + params[key] = [u] if u else [] + +# Add output paths to params +params['save_results'] = True + +if 'analysis' in outputs: + params['output_path'] = outputs['analysis'] + params['Output_Path'] = outputs['analysis'] + params['Output_File'] = outputs['analysis'] + +if 'DataFrames' in outputs: + df_dir = outputs['DataFrames'] + params['output_dir'] = df_dir + params['Export_Dir'] = df_dir + params['Output_File'] = os.path.join(df_dir, f'{template_name}_output.csv') + +if 'figures' in outputs: + fig_dir = outputs['figures'] + params['figure_dir'] = fig_dir + params['Figure_Dir'] = fig_dir + params['Figure_File'] = os.path.join(fig_dir, f'{template_name}.png') + +if 'html' in outputs: + html_dir = outputs['html'] + params['html_dir'] = html_dir + params['Output_File'] = os.path.join(html_dir, f'{template_name}.html') + +# Save runtime parameters +with open('params.runtime.json', 'w') as f: + json.dump(params, f, indent=2) + +# Save clean params for Galaxy display +params_display = {k: v for k, v in params.items() + if k not in ['Output_File', 'Figure_File', 'output_dir', 'figure_dir']} +with open('config_used.json', 'w') as f: + json.dump(params_display, f, indent=2) + +print(f"[Runner] Saved runtime parameters") + +# ============================================================================ +# LOAD AND EXECUTE TEMPLATE - CRITICAL: THIS MUST BE COMPLETE +# ============================================================================ + +# Try to import from installed package first (Docker environment) +template_module_name = template_filename.replace('.py', '') +try: + import importlib + mod = importlib.import_module(f'spac.templates.{template_module_name}') + print(f"[Runner] Loaded template from package: spac.templates.{template_module_name}") +except (ImportError, ModuleNotFoundError): + # Fallback to loading from file + print(f"[Runner] Package import failed, trying file load") + import importlib.util + + # Try standard locations + template_paths = [ + f'/app/spac/templates/{template_filename}', + f'/opt/spac/templates/{template_filename}', + f'/opt/SCSAWorkflow/src/spac/templates/{template_filename}', + template_filename # Current directory + ] + + spec = None + for path in template_paths: + if os.path.exists(path): + spec = importlib.util.spec_from_file_location("template_mod", path) + if spec: + print(f"[Runner] Found template at: {path}") + break + + if not spec or not spec.loader: + print(f"[Runner] ERROR: Could not find template: {template_filename}") + sys.exit(1) + + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + +# Verify run_from_json exists +if not hasattr(mod, 'run_from_json'): + print('[Runner] ERROR: Template missing run_from_json function') + sys.exit(2) + +# Check function signature +sig = inspect.signature(mod.run_from_json) +kwargs = {} + +if 'save_results' in sig.parameters: + kwargs['save_results'] = True +if 'show_plot' in sig.parameters: + kwargs['show_plot'] = False + +print(f"[Runner] Executing template with kwargs: {kwargs}") + +# Execute template +try: + result = mod.run_from_json('params.runtime.json', **kwargs) + print(f"[Runner] Template completed, returned: {type(result).__name__}") + + # Handle different return types + if result is not None: + if isinstance(result, dict): + print(f"[Runner] Template saved files: {list(result.keys())}") + elif isinstance(result, tuple): + # Handle tuple returns + saved_count = 0 + for i, item in enumerate(result): + if hasattr(item, 'savefig') and 'figures' in outputs: + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + fig_path = os.path.join(outputs['figures'], f'figure_{i+1}.png') + item.savefig(fig_path, dpi=300, bbox_inches='tight') + plt.close(item) + saved_count += 1 + print(f"[Runner] Saved figure to {fig_path}") + elif hasattr(item, 'to_csv') and 'DataFrames' in outputs: + df_path = os.path.join(outputs['DataFrames'], f'table_{i+1}.csv') + item.to_csv(df_path, index=True) + saved_count += 1 + print(f"[Runner] Saved DataFrame to {df_path}") + + if saved_count > 0: + print(f"[Runner] Saved {saved_count} in-memory results") + + elif hasattr(result, 'to_csv') and 'DataFrames' in outputs: + df_path = os.path.join(outputs['DataFrames'], 'output.csv') + result.to_csv(df_path, index=True) + print(f"[Runner] Saved DataFrame to {df_path}") + + elif hasattr(result, 'savefig') and 'figures' in outputs: + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + fig_path = os.path.join(outputs['figures'], 'figure.png') + result.savefig(fig_path, dpi=300, bbox_inches='tight') + plt.close(result) + print(f"[Runner] Saved figure to {fig_path}") + + elif hasattr(result, 'write_h5ad') and 'analysis' in outputs: + result.write_h5ad(outputs['analysis']) + print(f"[Runner] Saved AnnData to {outputs['analysis']}") + +except Exception as e: + print(f"[Runner] ERROR in template execution: {e}") + traceback.print_exc() + sys.exit(1) + +# Verify outputs +print("[Runner] Verifying outputs...") +found_outputs = False + +for output_type, path in outputs.items(): + if output_type == 'analysis': + if os.path.exists(path): + size = os.path.getsize(path) + print(f"[Runner] ✔ {output_type}: {path} ({size:,} bytes)") + found_outputs = True + else: + print(f"[Runner] ✗ {output_type}: NOT FOUND") + else: + if os.path.exists(path) and os.path.isdir(path): + files = os.listdir(path) + if files: + print(f"[Runner] ✔ {output_type}: {len(files)} files") + for f in files[:3]: + print(f"[Runner] - {f}") + if len(files) > 3: + print(f"[Runner] ... and {len(files)-3} more") + found_outputs = True + else: + print(f"[Runner] ⚠ {output_type}: directory empty") + +# Check for files in working directory and move them +print("[Runner] Checking for files in working directory...") +for file in os.listdir('.'): + if os.path.isdir(file) or file in ['params.runtime.json', 'config_used.json', + 'tool_stdout.txt', 'outputs_returned.json']: + continue + + if file.endswith('.csv') and 'DataFrames' in outputs: + if not os.path.exists(os.path.join(outputs['DataFrames'], file)): + target = os.path.join(outputs['DataFrames'], file) + shutil.move(file, target) + print(f"[Runner] Moved {file} to {target}") + found_outputs = True + elif file.endswith(('.png', '.pdf', '.jpg', '.svg')) and 'figures' in outputs: + if not os.path.exists(os.path.join(outputs['figures'], file)): + target = os.path.join(outputs['figures'], file) + shutil.move(file, target) + print(f"[Runner] Moved {file} to {target}") + found_outputs = True + +if found_outputs: + print("[Runner] === SUCCESS ===") +else: + print("[Runner] WARNING: No outputs created") + +PYTHON_RUNNER + +EXIT_CODE=$? + +if [ $EXIT_CODE -ne 0 ]; then + echo "ERROR: Template execution failed with exit code $EXIT_CODE" + exit 1 +fi + +echo "=== Execution Complete ===" +exit 0 \ No newline at end of file diff --git a/galaxy_tools/spac_load_csv_files/spac_load_csv_files.xml b/galaxy_tools/spac_load_csv_files/spac_load_csv_files.xml new file mode 100644 index 00000000..44901319 --- /dev/null +++ b/galaxy_tools/spac_load_csv_files/spac_load_csv_files.xml @@ -0,0 +1,84 @@ + + Load CSV files from NIDAP dataset and combine them into a single pandas dataframe for downstream ... + + + nciccbr/spac:v1 + + + + python3 + + + tool_stdout.txt && + + ## Run the universal wrapper + bash $__tool_directory__/run_spac_template.sh "$params_json" load_csv_files_with_config + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @misc{spac_toolkit, + author = {FNLCR DMAP Team}, + title = {SPAC: SPAtial single-Cell analysis}, + year = {2024}, + url = {https://github.com/FNLCR-DMAP/SCSAWorkflow} + } + + + \ No newline at end of file From bb6834bc2d292604f7499e09e9245384a3b0f694 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Thu, 2 Oct 2025 16:14:54 -0400 Subject: [PATCH 085/102] feat: add spac_setup_analysis galaxy tools --- .../spac_setup_analysis/run_spac_template.sh | 710 ++++++++++++++++++ .../spac_setup_analysis.xml | 66 ++ 2 files changed, 776 insertions(+) create mode 100644 galaxy_tools/spac_setup_analysis/run_spac_template.sh create mode 100644 galaxy_tools/spac_setup_analysis/spac_setup_analysis.xml diff --git a/galaxy_tools/spac_setup_analysis/run_spac_template.sh b/galaxy_tools/spac_setup_analysis/run_spac_template.sh new file mode 100644 index 00000000..a93b2d6e --- /dev/null +++ b/galaxy_tools/spac_setup_analysis/run_spac_template.sh @@ -0,0 +1,710 @@ +#!/usr/bin/env bash +# run_spac_template.sh - SPAC wrapper with column index conversion +# Version: 5.4.1 - Integrated column conversion +set -euo pipefail + +PARAMS_JSON="${1:?Missing params.json path}" +TEMPLATE_BASE="${2:?Missing template base name}" + +# Handle both base names and full .py filenames +if [[ "$TEMPLATE_BASE" == *.py ]]; then + TEMPLATE_PY="$TEMPLATE_BASE" +elif [[ "$TEMPLATE_BASE" == "load_csv_files_with_config" ]]; then + TEMPLATE_PY="load_csv_files_with_config.py" +else + TEMPLATE_PY="${TEMPLATE_BASE}_template.py" +fi + +# Use SPAC Python environment +SPAC_PYTHON="${SPAC_PYTHON:-python3}" + +echo "=== SPAC Template Wrapper v5.3 ===" +echo "Parameters: $PARAMS_JSON" +echo "Template base: $TEMPLATE_BASE" +echo "Template file: $TEMPLATE_PY" +echo "Python: $SPAC_PYTHON" + +# Run template through Python +"$SPAC_PYTHON" - <<'PYTHON_RUNNER' "$PARAMS_JSON" "$TEMPLATE_PY" 2>&1 | tee tool_stdout.txt +import json +import os +import sys +import copy +import traceback +import inspect +import shutil +import re +import csv + +# Get arguments +params_path = sys.argv[1] +template_filename = sys.argv[2] + +print(f"[Runner] Loading parameters from: {params_path}") +print(f"[Runner] Template: {template_filename}") + +# Load parameters +with open(params_path, 'r') as f: + params = json.load(f) + +# Extract template name +template_name = os.path.basename(template_filename).replace('_template.py', '').replace('.py', '') + +# =========================================================================== +# DE-SANITIZATION AND PARSING +# =========================================================================== +def _unsanitize(s: str) -> str: + """Remove Galaxy's parameter sanitization tokens""" + if not isinstance(s, str): + return s + replacements = { + '__ob__': '[', '__cb__': ']', + '__oc__': '{', '__cc__': '}', + '__dq__': '"', '__sq__': "'", + '__gt__': '>', '__lt__': '<', + '__cn__': '\n', '__cr__': '\r', + '__tc__': '\t', '__pd__': '#', + '__at__': '@', '__cm__': ',' + } + for token, char in replacements.items(): + s = s.replace(token, char) + return s + +def _maybe_parse(v): + """Recursively de-sanitize and JSON-parse strings where possible.""" + if isinstance(v, str): + u = _unsanitize(v).strip() + if (u.startswith('[') and u.endswith(']')) or (u.startswith('{') and u.endswith('}')): + try: + return json.loads(u) + except Exception: + return u + return u + elif isinstance(v, dict): + return {k: _maybe_parse(val) for k, val in v.items()} + elif isinstance(v, list): + return [_maybe_parse(item) for item in v] + return v + +# Normalize the whole params tree +params = _maybe_parse(params) + +# =========================================================================== +# COLUMN INDEX CONVERSION - CRITICAL FOR SETUP ANALYSIS +# =========================================================================== +def should_skip_column_conversion(template_name): + """Some templates don't need column index conversion""" + return 'load_csv' in template_name + +def read_file_headers(filepath): + """Read column headers from various file formats""" + try: + import pandas as pd + + # Try pandas auto-detect + try: + df = pd.read_csv(filepath, nrows=1) + if len(df.columns) > 1 or not df.columns[0].startswith('Unnamed'): + columns = df.columns.tolist() + print(f"[Runner] Pandas auto-detected delimiter, found {len(columns)} columns") + return columns + except: + pass + + # Try common delimiters + for sep in ['\t', ',', ';', '|', ' ']: + try: + df = pd.read_csv(filepath, sep=sep, nrows=1) + if len(df.columns) > 1: + columns = df.columns.tolist() + sep_name = {'\t': 'tab', ',': 'comma', ';': 'semicolon', + '|': 'pipe', ' ': 'space'}.get(sep, sep) + print(f"[Runner] Pandas found {sep_name}-delimited file with {len(columns)} columns") + return columns + except: + continue + except ImportError: + print("[Runner] pandas not available, using csv fallback") + + # CSV module fallback + try: + with open(filepath, 'r', encoding='utf-8', errors='replace', newline='') as f: + sample = f.read(8192) + f.seek(0) + + try: + dialect = csv.Sniffer().sniff(sample, delimiters='\t,;| ') + reader = csv.reader(f, dialect) + header = next(reader) + columns = [h.strip().strip('"') for h in header if h.strip()] + if columns: + print(f"[Runner] csv.Sniffer detected {len(columns)} columns") + return columns + except: + f.seek(0) + first_line = f.readline().strip() + for sep in ['\t', ',', ';', '|']: + if sep in first_line: + columns = [h.strip().strip('"') for h in first_line.split(sep)] + if len(columns) > 1: + print(f"[Runner] Manual parsing found {len(columns)} columns") + return columns + except Exception as e: + print(f"[Runner] Failed to read headers: {e}") + + return None + +def should_convert_param(key, value): + """Check if parameter contains column indices""" + if value is None or value == "" or value == [] or value == {}: + return False + + key_lower = key.lower() + + # Skip String_Columns - it's names not indices + if key == 'String_Columns': + return False + + # Skip output/path parameters + if any(x in key_lower for x in ['output', 'path', 'file', 'directory', 'save', 'export']): + return False + + # Skip regex/pattern parameters (but we'll handle Feature_Regex specially) + if 'regex' in key_lower or 'pattern' in key_lower: + return False + + # Parameters with 'column' likely have indices + if 'column' in key_lower or '_col' in key_lower: + return True + + # Known index parameters + if key in {'Annotation_s_', 'Features_to_Analyze', 'Features', 'Markers', 'Markers_to_Plot', 'Phenotypes'}: + return True + + # Check if values look like indices + if isinstance(value, list): + return all(isinstance(v, int) or (isinstance(v, str) and v.strip().isdigit()) for v in value if v) + elif isinstance(value, (int, str)): + return isinstance(value, int) or (isinstance(value, str) and value.strip().isdigit()) + + return False + +def convert_single_index(item, columns): + """Convert a single column index to name""" + if isinstance(item, str) and not item.strip().isdigit(): + return item + + try: + if isinstance(item, str): + item = int(item.strip()) + elif isinstance(item, float): + item = int(item) + except (ValueError, AttributeError): + return item + + if isinstance(item, int): + idx = item - 1 # Galaxy uses 1-based indexing + if 0 <= idx < len(columns): + return columns[idx] + elif 0 <= item < len(columns): # Fallback for 0-based + print(f"[Runner] Note: Found 0-based index {item}, converting to {columns[item]}") + return columns[item] + else: + print(f"[Runner] Warning: Index {item} out of range (have {len(columns)} columns)") + + return item + +def convert_column_indices_to_names(params, template_name): + """Convert column indices to names for templates that need it""" + + if should_skip_column_conversion(template_name): + print(f"[Runner] Skipping column conversion for {template_name}") + return params + + print(f"[Runner] Checking for column index conversion (template: {template_name})") + + # Find input file + input_file = None + input_keys = ['Upstream_Dataset', 'Upstream_Analysis', 'CSV_Files', + 'Input_File', 'Input_Dataset', 'Data_File'] + + for key in input_keys: + if key in params: + value = params[key] + if isinstance(value, list) and value: + value = value[0] + if value and os.path.exists(str(value)): + input_file = str(value) + print(f"[Runner] Found input file via {key}: {os.path.basename(input_file)}") + break + + if not input_file: + print("[Runner] No input file found for column conversion") + return params + + # Read headers + columns = read_file_headers(input_file) + if not columns: + print("[Runner] Could not read column headers, skipping conversion") + return params + + print(f"[Runner] Successfully read {len(columns)} columns") + if len(columns) <= 10: + print(f"[Runner] Columns: {columns}") + else: + print(f"[Runner] First 10 columns: {columns[:10]}") + + # Convert indices to names + converted_count = 0 + for key, value in params.items(): + # Skip non-column parameters + if not should_convert_param(key, value): + continue + + # Convert indices + if isinstance(value, list): + converted_items = [] + for item in value: + converted = convert_single_index(item, columns) + if converted is not None: + converted_items.append(converted) + converted_value = converted_items + else: + converted_value = convert_single_index(value, columns) + + if value != converted_value: + params[key] = converted_value + converted_count += 1 + print(f"[Runner] Converted {key}: {value} -> {converted_value}") + + if converted_count > 0: + print(f"[Runner] Total conversions: {converted_count} parameters") + + # CRITICAL: Handle Feature_Regex specially + if 'Feature_Regex' in params: + value = params['Feature_Regex'] + if value in [[], [""], "__ob____cb__", "[]", "", None]: + params['Feature_Regex'] = "" + print("[Runner] Cleared empty Feature_Regex parameter") + elif isinstance(value, list) and value: + params['Feature_Regex'] = "|".join(str(v) for v in value if v) + print(f"[Runner] Joined Feature_Regex list: {params['Feature_Regex']}") + + return params + +# =========================================================================== +# APPLY COLUMN CONVERSION +# =========================================================================== +print("[Runner] Step 1: Converting column indices to names") +params = convert_column_indices_to_names(params, template_name) + +# =========================================================================== +# SPECIAL HANDLING FOR SPECIFIC TEMPLATES +# =========================================================================== + +# Helper function to coerce singleton lists to strings for load_csv +def _coerce_singleton_paths_for_load_csv(params, template_name): + """For load_csv templates, flatten 1-item lists to strings for path-like params.""" + if 'load_csv' not in template_name: + return params + for key in ('CSV_Files', 'CSV_Files_Configuration'): + val = params.get(key) + if isinstance(val, list) and len(val) == 1 and isinstance(val[0], (str, bytes)): + params[key] = val[0] + print(f"[Runner] Coerced {key} from list -> string") + return params + +# Special handling for String_Columns in load_csv templates +if 'load_csv' in template_name and 'String_Columns' in params: + value = params['String_Columns'] + if not isinstance(value, list): + if value in [None, "", "[]", "__ob____cb__"]: + params['String_Columns'] = [] + elif isinstance(value, str): + s = value.strip() + if s.startswith('[') and s.endswith(']'): + try: + params['String_Columns'] = json.loads(s) + except: + params['String_Columns'] = [s] if s else [] + elif ',' in s: + params['String_Columns'] = [item.strip() for item in s.split(',') if item.strip()] + else: + params['String_Columns'] = [s] if s else [] + else: + params['String_Columns'] = [] + print(f"[Runner] Ensured String_Columns is list: {params['String_Columns']}") + +# Apply coercion for load_csv files +params = _coerce_singleton_paths_for_load_csv(params, template_name) + +# Fix for Load CSV Files directory +if 'load_csv' in template_name and 'CSV_Files' in params: + # Check if csv_input_dir was created by Galaxy command + if os.path.exists('csv_input_dir') and os.path.isdir('csv_input_dir'): + params['CSV_Files'] = 'csv_input_dir' + print("[Runner] Using csv_input_dir created by Galaxy") + elif isinstance(params['CSV_Files'], str) and os.path.isfile(params['CSV_Files']): + # We have a single file path, need to get its directory + params['CSV_Files'] = os.path.dirname(params['CSV_Files']) + print(f"[Runner] Using directory of CSV file: {params['CSV_Files']}") + +# =========================================================================== +# LIST PARAMETER NORMALIZATION +# =========================================================================== +def should_normalize_as_list(key, value): + """Determine if a parameter should be normalized as a list""" + if isinstance(value, list): + return True + + if value is None or value == "": + return False + + key_lower = key.lower() + + # Skip regex parameters + if 'regex' in key_lower or 'pattern' in key_lower: + return False + + # Skip known single-value parameters + if any(x in key_lower for x in ['single', 'one', 'first', 'second', 'primary']): + return False + + # Plural forms suggest lists + if any(x in key_lower for x in ['features', 'markers', 'phenotypes', 'annotations', + 'columns', 'types', 'labels', 'regions', 'radii']): + return True + + # Check for list separators + if isinstance(value, str): + if ',' in value or '\n' in value: + return True + if value.strip().startswith('[') and value.strip().endswith(']'): + return True + + return False + +def normalize_to_list(value): + """Convert various input formats to a proper Python list""" + if value in (None, "", "All", ["All"], "all", ["all"]): + return ["All"] + + if isinstance(value, list): + return value + + if isinstance(value, str): + s = value.strip() + + # Try JSON parsing + if s.startswith('[') and s.endswith(']'): + try: + parsed = json.loads(s) + return parsed if isinstance(parsed, list) else [str(parsed)] + except: + pass + + # Split by comma + if ',' in s: + return [item.strip() for item in s.split(',') if item.strip()] + + # Split by newline + if '\n' in s: + return [item.strip() for item in s.split('\n') if item.strip()] + + # Single value + return [s] if s else [] + + return [value] if value is not None else [] + +# Normalize list parameters +print("[Runner] Step 2: Normalizing list parameters") +list_count = 0 +for key, value in list(params.items()): + if should_normalize_as_list(key, value): + original = value + normalized = normalize_to_list(value) + if original != normalized: + params[key] = normalized + list_count += 1 + if len(str(normalized)) > 100: + print(f"[Runner] Normalized {key}: {type(original).__name__} -> list of {len(normalized)} items") + else: + print(f"[Runner] Normalized {key}: {original} -> {normalized}") + +if list_count > 0: + print(f"[Runner] Normalized {list_count} list parameters") + +# CRITICAL FIX: Handle single-element lists for coordinate columns +# These should be strings, not lists +coordinate_keys = ['X_Coordinate_Column', 'Y_Coordinate_Column', 'X_centroid', 'Y_centroid'] +for key in coordinate_keys: + if key in params: + value = params[key] + if isinstance(value, list) and len(value) == 1: + params[key] = value[0] + print(f"[Runner] Extracted single value from {key}: {value} -> {params[key]}") + +# Also check for any key ending with '_Column' that has a single-element list +for key in list(params.keys()): + if key.endswith('_Column') and isinstance(params[key], list) and len(params[key]) == 1: + original = params[key] + params[key] = params[key][0] + print(f"[Runner] Extracted single value from {key}: {original} -> {params[key]}") + +# =========================================================================== +# OUTPUTS HANDLING +# =========================================================================== + +# Extract outputs specification +raw_outputs = params.pop('outputs', {}) +outputs = {} + +if isinstance(raw_outputs, dict): + outputs = raw_outputs +elif isinstance(raw_outputs, str): + try: + maybe = json.loads(_unsanitize(raw_outputs)) + if isinstance(maybe, dict): + outputs = maybe + except Exception: + pass + +if not isinstance(outputs, dict) or not outputs: + print("[Runner] Warning: 'outputs' missing or not a dict; using defaults") + if 'boxplot' in template_name or 'plot' in template_name or 'histogram' in template_name: + outputs = {'DataFrames': 'dataframe_folder', 'figures': 'figure_folder'} + elif 'load_csv' in template_name: + outputs = {'DataFrames': 'dataframe_folder'} + elif 'interactive' in template_name: + outputs = {'html': 'html_folder'} + else: + outputs = {'analysis': 'transform_output.pickle'} + +print(f"[Runner] Outputs -> {list(outputs.keys())}") + +# Create output directories +for output_type, path in outputs.items(): + if output_type != 'analysis' and path: + os.makedirs(path, exist_ok=True) + print(f"[Runner] Created {output_type} directory: {path}") + +# Add output paths to params +params['save_results'] = True + +if 'analysis' in outputs: + params['output_path'] = outputs['analysis'] + params['Output_Path'] = outputs['analysis'] + params['Output_File'] = outputs['analysis'] + +if 'DataFrames' in outputs: + df_dir = outputs['DataFrames'] + params['output_dir'] = df_dir + params['Export_Dir'] = df_dir + params['Output_File'] = os.path.join(df_dir, f'{template_name}_output.csv') + +if 'figures' in outputs: + fig_dir = outputs['figures'] + params['figure_dir'] = fig_dir + params['Figure_Dir'] = fig_dir + params['Figure_File'] = os.path.join(fig_dir, f'{template_name}.png') + +if 'html' in outputs: + html_dir = outputs['html'] + params['html_dir'] = html_dir + params['Output_File'] = os.path.join(html_dir, f'{template_name}.html') + +# Save runtime parameters +with open('params.runtime.json', 'w') as f: + json.dump(params, f, indent=2) + +# Save clean params for Galaxy display +params_display = {k: v for k, v in params.items() + if k not in ['Output_File', 'Figure_File', 'output_dir', 'figure_dir']} +with open('config_used.json', 'w') as f: + json.dump(params_display, f, indent=2) + +print(f"[Runner] Saved runtime parameters") + +# ============================================================================ +# LOAD AND EXECUTE TEMPLATE +# ============================================================================ + +# Try to import from installed package first (Docker environment) +template_module_name = template_filename.replace('.py', '') +try: + import importlib + mod = importlib.import_module(f'spac.templates.{template_module_name}') + print(f"[Runner] Loaded template from package: spac.templates.{template_module_name}") +except (ImportError, ModuleNotFoundError): + # Fallback to loading from file + print(f"[Runner] Package import failed, trying file load") + import importlib.util + + # Standard locations + template_paths = [ + f'/app/spac/templates/{template_filename}', + f'/opt/spac/templates/{template_filename}', + f'/opt/SCSAWorkflow/src/spac/templates/{template_filename}', + template_filename # Current directory + ] + + spec = None + for path in template_paths: + if os.path.exists(path): + spec = importlib.util.spec_from_file_location("template_mod", path) + if spec: + print(f"[Runner] Found template at: {path}") + break + + if not spec or not spec.loader: + print(f"[Runner] ERROR: Could not find template: {template_filename}") + sys.exit(1) + + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + +# Verify run_from_json exists +if not hasattr(mod, 'run_from_json'): + print('[Runner] ERROR: Template missing run_from_json function') + sys.exit(2) + +# Check function signature +sig = inspect.signature(mod.run_from_json) +kwargs = {} + +if 'save_results' in sig.parameters: + kwargs['save_results'] = True +if 'show_plot' in sig.parameters: + kwargs['show_plot'] = False + +print(f"[Runner] Executing template with kwargs: {kwargs}") + +# Execute template +try: + result = mod.run_from_json('params.runtime.json', **kwargs) + print(f"[Runner] Template completed, returned: {type(result).__name__}") + + # Handle different return types + if result is not None: + if isinstance(result, dict): + print(f"[Runner] Template saved files: {list(result.keys())}") + elif isinstance(result, tuple): + # Handle tuple returns + saved_count = 0 + for i, item in enumerate(result): + if hasattr(item, 'savefig') and 'figures' in outputs: + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + fig_path = os.path.join(outputs['figures'], f'figure_{i+1}.png') + item.savefig(fig_path, dpi=300, bbox_inches='tight') + plt.close(item) + saved_count += 1 + print(f"[Runner] Saved figure to {fig_path}") + elif hasattr(item, 'to_csv') and 'DataFrames' in outputs: + df_path = os.path.join(outputs['DataFrames'], f'table_{i+1}.csv') + item.to_csv(df_path, index=True) + saved_count += 1 + print(f"[Runner] Saved DataFrame to {df_path}") + + if saved_count > 0: + print(f"[Runner] Saved {saved_count} in-memory results") + + elif hasattr(result, 'to_csv') and 'DataFrames' in outputs: + df_path = os.path.join(outputs['DataFrames'], 'output.csv') + result.to_csv(df_path, index=True) + print(f"[Runner] Saved DataFrame to {df_path}") + + elif hasattr(result, 'savefig') and 'figures' in outputs: + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + fig_path = os.path.join(outputs['figures'], 'figure.png') + result.savefig(fig_path, dpi=300, bbox_inches='tight') + plt.close(result) + print(f"[Runner] Saved figure to {fig_path}") + + elif hasattr(result, 'write_h5ad') and 'analysis' in outputs: + result.write_h5ad(outputs['analysis']) + print(f"[Runner] Saved AnnData to {outputs['analysis']}") + +except Exception as e: + print(f"[Runner] ERROR in template execution: {e}") + print(f"[Runner] Error type: {type(e).__name__}") + traceback.print_exc() + + # Debug help for common issues + if "String Columns must be a *list*" in str(e): + print("\n[Runner] DEBUG: String_Columns validation failed") + print(f"[Runner] Current String_Columns value: {params.get('String_Columns')}") + print(f"[Runner] Type: {type(params.get('String_Columns'))}") + + elif "regex pattern" in str(e).lower() or "^8$" in str(e): + print("\n[Runner] DEBUG: This appears to be a column index issue") + print("[Runner] Check that column indices were properly converted to names") + print("[Runner] Current Features_to_Analyze value:", params.get('Features_to_Analyze')) + print("[Runner] Current Feature_Regex value:", params.get('Feature_Regex')) + + sys.exit(1) + +# Verify outputs +print("[Runner] Verifying outputs...") +found_outputs = False + +for output_type, path in outputs.items(): + if output_type == 'analysis': + if os.path.exists(path): + size = os.path.getsize(path) + print(f"[Runner] ✔ {output_type}: {path} ({size:,} bytes)") + found_outputs = True + else: + print(f"[Runner] ✗ {output_type}: NOT FOUND") + else: + if os.path.exists(path) and os.path.isdir(path): + files = os.listdir(path) + if files: + print(f"[Runner] ✔ {output_type}: {len(files)} files") + for f in files[:3]: + print(f"[Runner] - {f}") + if len(files) > 3: + print(f"[Runner] ... and {len(files)-3} more") + found_outputs = True + else: + print(f"[Runner] ⚠ {output_type}: directory empty") + +# Check for files in working directory and move them +print("[Runner] Checking for files in working directory...") +for file in os.listdir('.'): + if os.path.isdir(file) or file in ['params.runtime.json', 'config_used.json', + 'tool_stdout.txt', 'outputs_returned.json']: + continue + + if file.endswith('.csv') and 'DataFrames' in outputs: + if not os.path.exists(os.path.join(outputs['DataFrames'], file)): + target = os.path.join(outputs['DataFrames'], file) + shutil.move(file, target) + print(f"[Runner] Moved {file} to {target}") + found_outputs = True + elif file.endswith(('.png', '.pdf', '.jpg', '.svg')) and 'figures' in outputs: + if not os.path.exists(os.path.join(outputs['figures'], file)): + target = os.path.join(outputs['figures'], file) + shutil.move(file, target) + print(f"[Runner] Moved {file} to {target}") + found_outputs = True + +if found_outputs: + print("[Runner] === SUCCESS ===") +else: + print("[Runner] WARNING: No outputs created") + +PYTHON_RUNNER + +EXIT_CODE=$? + +if [ $EXIT_CODE -ne 0 ]; then + echo "ERROR: Template execution failed with exit code $EXIT_CODE" + exit 1 +fi + +echo "=== Execution Complete ===" +exit 0 \ No newline at end of file diff --git a/galaxy_tools/spac_setup_analysis/spac_setup_analysis.xml b/galaxy_tools/spac_setup_analysis/spac_setup_analysis.xml new file mode 100644 index 00000000..495887e7 --- /dev/null +++ b/galaxy_tools/spac_setup_analysis/spac_setup_analysis.xml @@ -0,0 +1,66 @@ + + Convert the pre-processed dataset to the analysis object for downstream analysis. + + + nciccbr/spac:v1 + + + + python3 + + + tool_stdout.txt && + + ## Run the universal wrapper (template name without .py extension) + bash $__tool_directory__/run_spac_template.sh "$params_json" setup_analysis + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @misc{spac_toolkit, + author = {FNLCR DMAP Team}, + title = {SPAC: SPAtial single-Cell analysis}, + year = {2024}, + url = {https://github.com/FNLCR-DMAP/SCSAWorkflow} + } + + + \ No newline at end of file From c87e782ff03c3ec52a2ae4c4353f5b426a6fc9d0 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Thu, 2 Oct 2025 16:48:02 -0400 Subject: [PATCH 086/102] fix: spac_boxplot outputs in json and validated ha5d --- .../spac_boxplot_update/run_spac_template.sh | 710 ++++++++++++++++++ .../spac_boxplot_update/spac_boxplot.xml | 84 +++ 2 files changed, 794 insertions(+) create mode 100644 galaxy_tools/spac_boxplot_update/run_spac_template.sh create mode 100644 galaxy_tools/spac_boxplot_update/spac_boxplot.xml diff --git a/galaxy_tools/spac_boxplot_update/run_spac_template.sh b/galaxy_tools/spac_boxplot_update/run_spac_template.sh new file mode 100644 index 00000000..a93b2d6e --- /dev/null +++ b/galaxy_tools/spac_boxplot_update/run_spac_template.sh @@ -0,0 +1,710 @@ +#!/usr/bin/env bash +# run_spac_template.sh - SPAC wrapper with column index conversion +# Version: 5.4.1 - Integrated column conversion +set -euo pipefail + +PARAMS_JSON="${1:?Missing params.json path}" +TEMPLATE_BASE="${2:?Missing template base name}" + +# Handle both base names and full .py filenames +if [[ "$TEMPLATE_BASE" == *.py ]]; then + TEMPLATE_PY="$TEMPLATE_BASE" +elif [[ "$TEMPLATE_BASE" == "load_csv_files_with_config" ]]; then + TEMPLATE_PY="load_csv_files_with_config.py" +else + TEMPLATE_PY="${TEMPLATE_BASE}_template.py" +fi + +# Use SPAC Python environment +SPAC_PYTHON="${SPAC_PYTHON:-python3}" + +echo "=== SPAC Template Wrapper v5.3 ===" +echo "Parameters: $PARAMS_JSON" +echo "Template base: $TEMPLATE_BASE" +echo "Template file: $TEMPLATE_PY" +echo "Python: $SPAC_PYTHON" + +# Run template through Python +"$SPAC_PYTHON" - <<'PYTHON_RUNNER' "$PARAMS_JSON" "$TEMPLATE_PY" 2>&1 | tee tool_stdout.txt +import json +import os +import sys +import copy +import traceback +import inspect +import shutil +import re +import csv + +# Get arguments +params_path = sys.argv[1] +template_filename = sys.argv[2] + +print(f"[Runner] Loading parameters from: {params_path}") +print(f"[Runner] Template: {template_filename}") + +# Load parameters +with open(params_path, 'r') as f: + params = json.load(f) + +# Extract template name +template_name = os.path.basename(template_filename).replace('_template.py', '').replace('.py', '') + +# =========================================================================== +# DE-SANITIZATION AND PARSING +# =========================================================================== +def _unsanitize(s: str) -> str: + """Remove Galaxy's parameter sanitization tokens""" + if not isinstance(s, str): + return s + replacements = { + '__ob__': '[', '__cb__': ']', + '__oc__': '{', '__cc__': '}', + '__dq__': '"', '__sq__': "'", + '__gt__': '>', '__lt__': '<', + '__cn__': '\n', '__cr__': '\r', + '__tc__': '\t', '__pd__': '#', + '__at__': '@', '__cm__': ',' + } + for token, char in replacements.items(): + s = s.replace(token, char) + return s + +def _maybe_parse(v): + """Recursively de-sanitize and JSON-parse strings where possible.""" + if isinstance(v, str): + u = _unsanitize(v).strip() + if (u.startswith('[') and u.endswith(']')) or (u.startswith('{') and u.endswith('}')): + try: + return json.loads(u) + except Exception: + return u + return u + elif isinstance(v, dict): + return {k: _maybe_parse(val) for k, val in v.items()} + elif isinstance(v, list): + return [_maybe_parse(item) for item in v] + return v + +# Normalize the whole params tree +params = _maybe_parse(params) + +# =========================================================================== +# COLUMN INDEX CONVERSION - CRITICAL FOR SETUP ANALYSIS +# =========================================================================== +def should_skip_column_conversion(template_name): + """Some templates don't need column index conversion""" + return 'load_csv' in template_name + +def read_file_headers(filepath): + """Read column headers from various file formats""" + try: + import pandas as pd + + # Try pandas auto-detect + try: + df = pd.read_csv(filepath, nrows=1) + if len(df.columns) > 1 or not df.columns[0].startswith('Unnamed'): + columns = df.columns.tolist() + print(f"[Runner] Pandas auto-detected delimiter, found {len(columns)} columns") + return columns + except: + pass + + # Try common delimiters + for sep in ['\t', ',', ';', '|', ' ']: + try: + df = pd.read_csv(filepath, sep=sep, nrows=1) + if len(df.columns) > 1: + columns = df.columns.tolist() + sep_name = {'\t': 'tab', ',': 'comma', ';': 'semicolon', + '|': 'pipe', ' ': 'space'}.get(sep, sep) + print(f"[Runner] Pandas found {sep_name}-delimited file with {len(columns)} columns") + return columns + except: + continue + except ImportError: + print("[Runner] pandas not available, using csv fallback") + + # CSV module fallback + try: + with open(filepath, 'r', encoding='utf-8', errors='replace', newline='') as f: + sample = f.read(8192) + f.seek(0) + + try: + dialect = csv.Sniffer().sniff(sample, delimiters='\t,;| ') + reader = csv.reader(f, dialect) + header = next(reader) + columns = [h.strip().strip('"') for h in header if h.strip()] + if columns: + print(f"[Runner] csv.Sniffer detected {len(columns)} columns") + return columns + except: + f.seek(0) + first_line = f.readline().strip() + for sep in ['\t', ',', ';', '|']: + if sep in first_line: + columns = [h.strip().strip('"') for h in first_line.split(sep)] + if len(columns) > 1: + print(f"[Runner] Manual parsing found {len(columns)} columns") + return columns + except Exception as e: + print(f"[Runner] Failed to read headers: {e}") + + return None + +def should_convert_param(key, value): + """Check if parameter contains column indices""" + if value is None or value == "" or value == [] or value == {}: + return False + + key_lower = key.lower() + + # Skip String_Columns - it's names not indices + if key == 'String_Columns': + return False + + # Skip output/path parameters + if any(x in key_lower for x in ['output', 'path', 'file', 'directory', 'save', 'export']): + return False + + # Skip regex/pattern parameters (but we'll handle Feature_Regex specially) + if 'regex' in key_lower or 'pattern' in key_lower: + return False + + # Parameters with 'column' likely have indices + if 'column' in key_lower or '_col' in key_lower: + return True + + # Known index parameters + if key in {'Annotation_s_', 'Features_to_Analyze', 'Features', 'Markers', 'Markers_to_Plot', 'Phenotypes'}: + return True + + # Check if values look like indices + if isinstance(value, list): + return all(isinstance(v, int) or (isinstance(v, str) and v.strip().isdigit()) for v in value if v) + elif isinstance(value, (int, str)): + return isinstance(value, int) or (isinstance(value, str) and value.strip().isdigit()) + + return False + +def convert_single_index(item, columns): + """Convert a single column index to name""" + if isinstance(item, str) and not item.strip().isdigit(): + return item + + try: + if isinstance(item, str): + item = int(item.strip()) + elif isinstance(item, float): + item = int(item) + except (ValueError, AttributeError): + return item + + if isinstance(item, int): + idx = item - 1 # Galaxy uses 1-based indexing + if 0 <= idx < len(columns): + return columns[idx] + elif 0 <= item < len(columns): # Fallback for 0-based + print(f"[Runner] Note: Found 0-based index {item}, converting to {columns[item]}") + return columns[item] + else: + print(f"[Runner] Warning: Index {item} out of range (have {len(columns)} columns)") + + return item + +def convert_column_indices_to_names(params, template_name): + """Convert column indices to names for templates that need it""" + + if should_skip_column_conversion(template_name): + print(f"[Runner] Skipping column conversion for {template_name}") + return params + + print(f"[Runner] Checking for column index conversion (template: {template_name})") + + # Find input file + input_file = None + input_keys = ['Upstream_Dataset', 'Upstream_Analysis', 'CSV_Files', + 'Input_File', 'Input_Dataset', 'Data_File'] + + for key in input_keys: + if key in params: + value = params[key] + if isinstance(value, list) and value: + value = value[0] + if value and os.path.exists(str(value)): + input_file = str(value) + print(f"[Runner] Found input file via {key}: {os.path.basename(input_file)}") + break + + if not input_file: + print("[Runner] No input file found for column conversion") + return params + + # Read headers + columns = read_file_headers(input_file) + if not columns: + print("[Runner] Could not read column headers, skipping conversion") + return params + + print(f"[Runner] Successfully read {len(columns)} columns") + if len(columns) <= 10: + print(f"[Runner] Columns: {columns}") + else: + print(f"[Runner] First 10 columns: {columns[:10]}") + + # Convert indices to names + converted_count = 0 + for key, value in params.items(): + # Skip non-column parameters + if not should_convert_param(key, value): + continue + + # Convert indices + if isinstance(value, list): + converted_items = [] + for item in value: + converted = convert_single_index(item, columns) + if converted is not None: + converted_items.append(converted) + converted_value = converted_items + else: + converted_value = convert_single_index(value, columns) + + if value != converted_value: + params[key] = converted_value + converted_count += 1 + print(f"[Runner] Converted {key}: {value} -> {converted_value}") + + if converted_count > 0: + print(f"[Runner] Total conversions: {converted_count} parameters") + + # CRITICAL: Handle Feature_Regex specially + if 'Feature_Regex' in params: + value = params['Feature_Regex'] + if value in [[], [""], "__ob____cb__", "[]", "", None]: + params['Feature_Regex'] = "" + print("[Runner] Cleared empty Feature_Regex parameter") + elif isinstance(value, list) and value: + params['Feature_Regex'] = "|".join(str(v) for v in value if v) + print(f"[Runner] Joined Feature_Regex list: {params['Feature_Regex']}") + + return params + +# =========================================================================== +# APPLY COLUMN CONVERSION +# =========================================================================== +print("[Runner] Step 1: Converting column indices to names") +params = convert_column_indices_to_names(params, template_name) + +# =========================================================================== +# SPECIAL HANDLING FOR SPECIFIC TEMPLATES +# =========================================================================== + +# Helper function to coerce singleton lists to strings for load_csv +def _coerce_singleton_paths_for_load_csv(params, template_name): + """For load_csv templates, flatten 1-item lists to strings for path-like params.""" + if 'load_csv' not in template_name: + return params + for key in ('CSV_Files', 'CSV_Files_Configuration'): + val = params.get(key) + if isinstance(val, list) and len(val) == 1 and isinstance(val[0], (str, bytes)): + params[key] = val[0] + print(f"[Runner] Coerced {key} from list -> string") + return params + +# Special handling for String_Columns in load_csv templates +if 'load_csv' in template_name and 'String_Columns' in params: + value = params['String_Columns'] + if not isinstance(value, list): + if value in [None, "", "[]", "__ob____cb__"]: + params['String_Columns'] = [] + elif isinstance(value, str): + s = value.strip() + if s.startswith('[') and s.endswith(']'): + try: + params['String_Columns'] = json.loads(s) + except: + params['String_Columns'] = [s] if s else [] + elif ',' in s: + params['String_Columns'] = [item.strip() for item in s.split(',') if item.strip()] + else: + params['String_Columns'] = [s] if s else [] + else: + params['String_Columns'] = [] + print(f"[Runner] Ensured String_Columns is list: {params['String_Columns']}") + +# Apply coercion for load_csv files +params = _coerce_singleton_paths_for_load_csv(params, template_name) + +# Fix for Load CSV Files directory +if 'load_csv' in template_name and 'CSV_Files' in params: + # Check if csv_input_dir was created by Galaxy command + if os.path.exists('csv_input_dir') and os.path.isdir('csv_input_dir'): + params['CSV_Files'] = 'csv_input_dir' + print("[Runner] Using csv_input_dir created by Galaxy") + elif isinstance(params['CSV_Files'], str) and os.path.isfile(params['CSV_Files']): + # We have a single file path, need to get its directory + params['CSV_Files'] = os.path.dirname(params['CSV_Files']) + print(f"[Runner] Using directory of CSV file: {params['CSV_Files']}") + +# =========================================================================== +# LIST PARAMETER NORMALIZATION +# =========================================================================== +def should_normalize_as_list(key, value): + """Determine if a parameter should be normalized as a list""" + if isinstance(value, list): + return True + + if value is None or value == "": + return False + + key_lower = key.lower() + + # Skip regex parameters + if 'regex' in key_lower or 'pattern' in key_lower: + return False + + # Skip known single-value parameters + if any(x in key_lower for x in ['single', 'one', 'first', 'second', 'primary']): + return False + + # Plural forms suggest lists + if any(x in key_lower for x in ['features', 'markers', 'phenotypes', 'annotations', + 'columns', 'types', 'labels', 'regions', 'radii']): + return True + + # Check for list separators + if isinstance(value, str): + if ',' in value or '\n' in value: + return True + if value.strip().startswith('[') and value.strip().endswith(']'): + return True + + return False + +def normalize_to_list(value): + """Convert various input formats to a proper Python list""" + if value in (None, "", "All", ["All"], "all", ["all"]): + return ["All"] + + if isinstance(value, list): + return value + + if isinstance(value, str): + s = value.strip() + + # Try JSON parsing + if s.startswith('[') and s.endswith(']'): + try: + parsed = json.loads(s) + return parsed if isinstance(parsed, list) else [str(parsed)] + except: + pass + + # Split by comma + if ',' in s: + return [item.strip() for item in s.split(',') if item.strip()] + + # Split by newline + if '\n' in s: + return [item.strip() for item in s.split('\n') if item.strip()] + + # Single value + return [s] if s else [] + + return [value] if value is not None else [] + +# Normalize list parameters +print("[Runner] Step 2: Normalizing list parameters") +list_count = 0 +for key, value in list(params.items()): + if should_normalize_as_list(key, value): + original = value + normalized = normalize_to_list(value) + if original != normalized: + params[key] = normalized + list_count += 1 + if len(str(normalized)) > 100: + print(f"[Runner] Normalized {key}: {type(original).__name__} -> list of {len(normalized)} items") + else: + print(f"[Runner] Normalized {key}: {original} -> {normalized}") + +if list_count > 0: + print(f"[Runner] Normalized {list_count} list parameters") + +# CRITICAL FIX: Handle single-element lists for coordinate columns +# These should be strings, not lists +coordinate_keys = ['X_Coordinate_Column', 'Y_Coordinate_Column', 'X_centroid', 'Y_centroid'] +for key in coordinate_keys: + if key in params: + value = params[key] + if isinstance(value, list) and len(value) == 1: + params[key] = value[0] + print(f"[Runner] Extracted single value from {key}: {value} -> {params[key]}") + +# Also check for any key ending with '_Column' that has a single-element list +for key in list(params.keys()): + if key.endswith('_Column') and isinstance(params[key], list) and len(params[key]) == 1: + original = params[key] + params[key] = params[key][0] + print(f"[Runner] Extracted single value from {key}: {original} -> {params[key]}") + +# =========================================================================== +# OUTPUTS HANDLING +# =========================================================================== + +# Extract outputs specification +raw_outputs = params.pop('outputs', {}) +outputs = {} + +if isinstance(raw_outputs, dict): + outputs = raw_outputs +elif isinstance(raw_outputs, str): + try: + maybe = json.loads(_unsanitize(raw_outputs)) + if isinstance(maybe, dict): + outputs = maybe + except Exception: + pass + +if not isinstance(outputs, dict) or not outputs: + print("[Runner] Warning: 'outputs' missing or not a dict; using defaults") + if 'boxplot' in template_name or 'plot' in template_name or 'histogram' in template_name: + outputs = {'DataFrames': 'dataframe_folder', 'figures': 'figure_folder'} + elif 'load_csv' in template_name: + outputs = {'DataFrames': 'dataframe_folder'} + elif 'interactive' in template_name: + outputs = {'html': 'html_folder'} + else: + outputs = {'analysis': 'transform_output.pickle'} + +print(f"[Runner] Outputs -> {list(outputs.keys())}") + +# Create output directories +for output_type, path in outputs.items(): + if output_type != 'analysis' and path: + os.makedirs(path, exist_ok=True) + print(f"[Runner] Created {output_type} directory: {path}") + +# Add output paths to params +params['save_results'] = True + +if 'analysis' in outputs: + params['output_path'] = outputs['analysis'] + params['Output_Path'] = outputs['analysis'] + params['Output_File'] = outputs['analysis'] + +if 'DataFrames' in outputs: + df_dir = outputs['DataFrames'] + params['output_dir'] = df_dir + params['Export_Dir'] = df_dir + params['Output_File'] = os.path.join(df_dir, f'{template_name}_output.csv') + +if 'figures' in outputs: + fig_dir = outputs['figures'] + params['figure_dir'] = fig_dir + params['Figure_Dir'] = fig_dir + params['Figure_File'] = os.path.join(fig_dir, f'{template_name}.png') + +if 'html' in outputs: + html_dir = outputs['html'] + params['html_dir'] = html_dir + params['Output_File'] = os.path.join(html_dir, f'{template_name}.html') + +# Save runtime parameters +with open('params.runtime.json', 'w') as f: + json.dump(params, f, indent=2) + +# Save clean params for Galaxy display +params_display = {k: v for k, v in params.items() + if k not in ['Output_File', 'Figure_File', 'output_dir', 'figure_dir']} +with open('config_used.json', 'w') as f: + json.dump(params_display, f, indent=2) + +print(f"[Runner] Saved runtime parameters") + +# ============================================================================ +# LOAD AND EXECUTE TEMPLATE +# ============================================================================ + +# Try to import from installed package first (Docker environment) +template_module_name = template_filename.replace('.py', '') +try: + import importlib + mod = importlib.import_module(f'spac.templates.{template_module_name}') + print(f"[Runner] Loaded template from package: spac.templates.{template_module_name}") +except (ImportError, ModuleNotFoundError): + # Fallback to loading from file + print(f"[Runner] Package import failed, trying file load") + import importlib.util + + # Standard locations + template_paths = [ + f'/app/spac/templates/{template_filename}', + f'/opt/spac/templates/{template_filename}', + f'/opt/SCSAWorkflow/src/spac/templates/{template_filename}', + template_filename # Current directory + ] + + spec = None + for path in template_paths: + if os.path.exists(path): + spec = importlib.util.spec_from_file_location("template_mod", path) + if spec: + print(f"[Runner] Found template at: {path}") + break + + if not spec or not spec.loader: + print(f"[Runner] ERROR: Could not find template: {template_filename}") + sys.exit(1) + + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + +# Verify run_from_json exists +if not hasattr(mod, 'run_from_json'): + print('[Runner] ERROR: Template missing run_from_json function') + sys.exit(2) + +# Check function signature +sig = inspect.signature(mod.run_from_json) +kwargs = {} + +if 'save_results' in sig.parameters: + kwargs['save_results'] = True +if 'show_plot' in sig.parameters: + kwargs['show_plot'] = False + +print(f"[Runner] Executing template with kwargs: {kwargs}") + +# Execute template +try: + result = mod.run_from_json('params.runtime.json', **kwargs) + print(f"[Runner] Template completed, returned: {type(result).__name__}") + + # Handle different return types + if result is not None: + if isinstance(result, dict): + print(f"[Runner] Template saved files: {list(result.keys())}") + elif isinstance(result, tuple): + # Handle tuple returns + saved_count = 0 + for i, item in enumerate(result): + if hasattr(item, 'savefig') and 'figures' in outputs: + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + fig_path = os.path.join(outputs['figures'], f'figure_{i+1}.png') + item.savefig(fig_path, dpi=300, bbox_inches='tight') + plt.close(item) + saved_count += 1 + print(f"[Runner] Saved figure to {fig_path}") + elif hasattr(item, 'to_csv') and 'DataFrames' in outputs: + df_path = os.path.join(outputs['DataFrames'], f'table_{i+1}.csv') + item.to_csv(df_path, index=True) + saved_count += 1 + print(f"[Runner] Saved DataFrame to {df_path}") + + if saved_count > 0: + print(f"[Runner] Saved {saved_count} in-memory results") + + elif hasattr(result, 'to_csv') and 'DataFrames' in outputs: + df_path = os.path.join(outputs['DataFrames'], 'output.csv') + result.to_csv(df_path, index=True) + print(f"[Runner] Saved DataFrame to {df_path}") + + elif hasattr(result, 'savefig') and 'figures' in outputs: + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + fig_path = os.path.join(outputs['figures'], 'figure.png') + result.savefig(fig_path, dpi=300, bbox_inches='tight') + plt.close(result) + print(f"[Runner] Saved figure to {fig_path}") + + elif hasattr(result, 'write_h5ad') and 'analysis' in outputs: + result.write_h5ad(outputs['analysis']) + print(f"[Runner] Saved AnnData to {outputs['analysis']}") + +except Exception as e: + print(f"[Runner] ERROR in template execution: {e}") + print(f"[Runner] Error type: {type(e).__name__}") + traceback.print_exc() + + # Debug help for common issues + if "String Columns must be a *list*" in str(e): + print("\n[Runner] DEBUG: String_Columns validation failed") + print(f"[Runner] Current String_Columns value: {params.get('String_Columns')}") + print(f"[Runner] Type: {type(params.get('String_Columns'))}") + + elif "regex pattern" in str(e).lower() or "^8$" in str(e): + print("\n[Runner] DEBUG: This appears to be a column index issue") + print("[Runner] Check that column indices were properly converted to names") + print("[Runner] Current Features_to_Analyze value:", params.get('Features_to_Analyze')) + print("[Runner] Current Feature_Regex value:", params.get('Feature_Regex')) + + sys.exit(1) + +# Verify outputs +print("[Runner] Verifying outputs...") +found_outputs = False + +for output_type, path in outputs.items(): + if output_type == 'analysis': + if os.path.exists(path): + size = os.path.getsize(path) + print(f"[Runner] ✔ {output_type}: {path} ({size:,} bytes)") + found_outputs = True + else: + print(f"[Runner] ✗ {output_type}: NOT FOUND") + else: + if os.path.exists(path) and os.path.isdir(path): + files = os.listdir(path) + if files: + print(f"[Runner] ✔ {output_type}: {len(files)} files") + for f in files[:3]: + print(f"[Runner] - {f}") + if len(files) > 3: + print(f"[Runner] ... and {len(files)-3} more") + found_outputs = True + else: + print(f"[Runner] ⚠ {output_type}: directory empty") + +# Check for files in working directory and move them +print("[Runner] Checking for files in working directory...") +for file in os.listdir('.'): + if os.path.isdir(file) or file in ['params.runtime.json', 'config_used.json', + 'tool_stdout.txt', 'outputs_returned.json']: + continue + + if file.endswith('.csv') and 'DataFrames' in outputs: + if not os.path.exists(os.path.join(outputs['DataFrames'], file)): + target = os.path.join(outputs['DataFrames'], file) + shutil.move(file, target) + print(f"[Runner] Moved {file} to {target}") + found_outputs = True + elif file.endswith(('.png', '.pdf', '.jpg', '.svg')) and 'figures' in outputs: + if not os.path.exists(os.path.join(outputs['figures'], file)): + target = os.path.join(outputs['figures'], file) + shutil.move(file, target) + print(f"[Runner] Moved {file} to {target}") + found_outputs = True + +if found_outputs: + print("[Runner] === SUCCESS ===") +else: + print("[Runner] WARNING: No outputs created") + +PYTHON_RUNNER + +EXIT_CODE=$? + +if [ $EXIT_CODE -ne 0 ]; then + echo "ERROR: Template execution failed with exit code $EXIT_CODE" + exit 1 +fi + +echo "=== Execution Complete ===" +exit 0 \ No newline at end of file diff --git a/galaxy_tools/spac_boxplot_update/spac_boxplot.xml b/galaxy_tools/spac_boxplot_update/spac_boxplot.xml new file mode 100644 index 00000000..9798c2c4 --- /dev/null +++ b/galaxy_tools/spac_boxplot_update/spac_boxplot.xml @@ -0,0 +1,84 @@ + + Create a boxplot visualization of the features in the analysis dataset. + + + nciccbr/spac:v1 + + + + python3 + + + tool_stdout.txt && + + ## Run the universal wrapper (template name without .py extension) + bash $__tool_directory__/run_spac_template.sh "$params_json" boxplot + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @misc{spac_toolkit, + author = {FNLCR DMAP Team}, + title = {SPAC: SPAtial single-Cell analysis}, + year = {2024}, + url = {https://github.com/FNLCR-DMAP/SCSAWorkflow} + } + + + \ No newline at end of file From cbbcd9e47930cf9b258a8668af6ec81238ad09d9 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Fri, 3 Oct 2025 00:30:58 -0400 Subject: [PATCH 087/102] feat: add spac_zscore_normalization galaxy tools --- .../run_spac_template.sh | 710 ++++++++++++++++++ .../spac_zscore_normalization.xml | 63 ++ 2 files changed, 773 insertions(+) create mode 100644 galaxy_tools/spac_zscore_normalization/run_spac_template.sh create mode 100644 galaxy_tools/spac_zscore_normalization/spac_zscore_normalization.xml diff --git a/galaxy_tools/spac_zscore_normalization/run_spac_template.sh b/galaxy_tools/spac_zscore_normalization/run_spac_template.sh new file mode 100644 index 00000000..a93b2d6e --- /dev/null +++ b/galaxy_tools/spac_zscore_normalization/run_spac_template.sh @@ -0,0 +1,710 @@ +#!/usr/bin/env bash +# run_spac_template.sh - SPAC wrapper with column index conversion +# Version: 5.4.1 - Integrated column conversion +set -euo pipefail + +PARAMS_JSON="${1:?Missing params.json path}" +TEMPLATE_BASE="${2:?Missing template base name}" + +# Handle both base names and full .py filenames +if [[ "$TEMPLATE_BASE" == *.py ]]; then + TEMPLATE_PY="$TEMPLATE_BASE" +elif [[ "$TEMPLATE_BASE" == "load_csv_files_with_config" ]]; then + TEMPLATE_PY="load_csv_files_with_config.py" +else + TEMPLATE_PY="${TEMPLATE_BASE}_template.py" +fi + +# Use SPAC Python environment +SPAC_PYTHON="${SPAC_PYTHON:-python3}" + +echo "=== SPAC Template Wrapper v5.3 ===" +echo "Parameters: $PARAMS_JSON" +echo "Template base: $TEMPLATE_BASE" +echo "Template file: $TEMPLATE_PY" +echo "Python: $SPAC_PYTHON" + +# Run template through Python +"$SPAC_PYTHON" - <<'PYTHON_RUNNER' "$PARAMS_JSON" "$TEMPLATE_PY" 2>&1 | tee tool_stdout.txt +import json +import os +import sys +import copy +import traceback +import inspect +import shutil +import re +import csv + +# Get arguments +params_path = sys.argv[1] +template_filename = sys.argv[2] + +print(f"[Runner] Loading parameters from: {params_path}") +print(f"[Runner] Template: {template_filename}") + +# Load parameters +with open(params_path, 'r') as f: + params = json.load(f) + +# Extract template name +template_name = os.path.basename(template_filename).replace('_template.py', '').replace('.py', '') + +# =========================================================================== +# DE-SANITIZATION AND PARSING +# =========================================================================== +def _unsanitize(s: str) -> str: + """Remove Galaxy's parameter sanitization tokens""" + if not isinstance(s, str): + return s + replacements = { + '__ob__': '[', '__cb__': ']', + '__oc__': '{', '__cc__': '}', + '__dq__': '"', '__sq__': "'", + '__gt__': '>', '__lt__': '<', + '__cn__': '\n', '__cr__': '\r', + '__tc__': '\t', '__pd__': '#', + '__at__': '@', '__cm__': ',' + } + for token, char in replacements.items(): + s = s.replace(token, char) + return s + +def _maybe_parse(v): + """Recursively de-sanitize and JSON-parse strings where possible.""" + if isinstance(v, str): + u = _unsanitize(v).strip() + if (u.startswith('[') and u.endswith(']')) or (u.startswith('{') and u.endswith('}')): + try: + return json.loads(u) + except Exception: + return u + return u + elif isinstance(v, dict): + return {k: _maybe_parse(val) for k, val in v.items()} + elif isinstance(v, list): + return [_maybe_parse(item) for item in v] + return v + +# Normalize the whole params tree +params = _maybe_parse(params) + +# =========================================================================== +# COLUMN INDEX CONVERSION - CRITICAL FOR SETUP ANALYSIS +# =========================================================================== +def should_skip_column_conversion(template_name): + """Some templates don't need column index conversion""" + return 'load_csv' in template_name + +def read_file_headers(filepath): + """Read column headers from various file formats""" + try: + import pandas as pd + + # Try pandas auto-detect + try: + df = pd.read_csv(filepath, nrows=1) + if len(df.columns) > 1 or not df.columns[0].startswith('Unnamed'): + columns = df.columns.tolist() + print(f"[Runner] Pandas auto-detected delimiter, found {len(columns)} columns") + return columns + except: + pass + + # Try common delimiters + for sep in ['\t', ',', ';', '|', ' ']: + try: + df = pd.read_csv(filepath, sep=sep, nrows=1) + if len(df.columns) > 1: + columns = df.columns.tolist() + sep_name = {'\t': 'tab', ',': 'comma', ';': 'semicolon', + '|': 'pipe', ' ': 'space'}.get(sep, sep) + print(f"[Runner] Pandas found {sep_name}-delimited file with {len(columns)} columns") + return columns + except: + continue + except ImportError: + print("[Runner] pandas not available, using csv fallback") + + # CSV module fallback + try: + with open(filepath, 'r', encoding='utf-8', errors='replace', newline='') as f: + sample = f.read(8192) + f.seek(0) + + try: + dialect = csv.Sniffer().sniff(sample, delimiters='\t,;| ') + reader = csv.reader(f, dialect) + header = next(reader) + columns = [h.strip().strip('"') for h in header if h.strip()] + if columns: + print(f"[Runner] csv.Sniffer detected {len(columns)} columns") + return columns + except: + f.seek(0) + first_line = f.readline().strip() + for sep in ['\t', ',', ';', '|']: + if sep in first_line: + columns = [h.strip().strip('"') for h in first_line.split(sep)] + if len(columns) > 1: + print(f"[Runner] Manual parsing found {len(columns)} columns") + return columns + except Exception as e: + print(f"[Runner] Failed to read headers: {e}") + + return None + +def should_convert_param(key, value): + """Check if parameter contains column indices""" + if value is None or value == "" or value == [] or value == {}: + return False + + key_lower = key.lower() + + # Skip String_Columns - it's names not indices + if key == 'String_Columns': + return False + + # Skip output/path parameters + if any(x in key_lower for x in ['output', 'path', 'file', 'directory', 'save', 'export']): + return False + + # Skip regex/pattern parameters (but we'll handle Feature_Regex specially) + if 'regex' in key_lower or 'pattern' in key_lower: + return False + + # Parameters with 'column' likely have indices + if 'column' in key_lower or '_col' in key_lower: + return True + + # Known index parameters + if key in {'Annotation_s_', 'Features_to_Analyze', 'Features', 'Markers', 'Markers_to_Plot', 'Phenotypes'}: + return True + + # Check if values look like indices + if isinstance(value, list): + return all(isinstance(v, int) or (isinstance(v, str) and v.strip().isdigit()) for v in value if v) + elif isinstance(value, (int, str)): + return isinstance(value, int) or (isinstance(value, str) and value.strip().isdigit()) + + return False + +def convert_single_index(item, columns): + """Convert a single column index to name""" + if isinstance(item, str) and not item.strip().isdigit(): + return item + + try: + if isinstance(item, str): + item = int(item.strip()) + elif isinstance(item, float): + item = int(item) + except (ValueError, AttributeError): + return item + + if isinstance(item, int): + idx = item - 1 # Galaxy uses 1-based indexing + if 0 <= idx < len(columns): + return columns[idx] + elif 0 <= item < len(columns): # Fallback for 0-based + print(f"[Runner] Note: Found 0-based index {item}, converting to {columns[item]}") + return columns[item] + else: + print(f"[Runner] Warning: Index {item} out of range (have {len(columns)} columns)") + + return item + +def convert_column_indices_to_names(params, template_name): + """Convert column indices to names for templates that need it""" + + if should_skip_column_conversion(template_name): + print(f"[Runner] Skipping column conversion for {template_name}") + return params + + print(f"[Runner] Checking for column index conversion (template: {template_name})") + + # Find input file + input_file = None + input_keys = ['Upstream_Dataset', 'Upstream_Analysis', 'CSV_Files', + 'Input_File', 'Input_Dataset', 'Data_File'] + + for key in input_keys: + if key in params: + value = params[key] + if isinstance(value, list) and value: + value = value[0] + if value and os.path.exists(str(value)): + input_file = str(value) + print(f"[Runner] Found input file via {key}: {os.path.basename(input_file)}") + break + + if not input_file: + print("[Runner] No input file found for column conversion") + return params + + # Read headers + columns = read_file_headers(input_file) + if not columns: + print("[Runner] Could not read column headers, skipping conversion") + return params + + print(f"[Runner] Successfully read {len(columns)} columns") + if len(columns) <= 10: + print(f"[Runner] Columns: {columns}") + else: + print(f"[Runner] First 10 columns: {columns[:10]}") + + # Convert indices to names + converted_count = 0 + for key, value in params.items(): + # Skip non-column parameters + if not should_convert_param(key, value): + continue + + # Convert indices + if isinstance(value, list): + converted_items = [] + for item in value: + converted = convert_single_index(item, columns) + if converted is not None: + converted_items.append(converted) + converted_value = converted_items + else: + converted_value = convert_single_index(value, columns) + + if value != converted_value: + params[key] = converted_value + converted_count += 1 + print(f"[Runner] Converted {key}: {value} -> {converted_value}") + + if converted_count > 0: + print(f"[Runner] Total conversions: {converted_count} parameters") + + # CRITICAL: Handle Feature_Regex specially + if 'Feature_Regex' in params: + value = params['Feature_Regex'] + if value in [[], [""], "__ob____cb__", "[]", "", None]: + params['Feature_Regex'] = "" + print("[Runner] Cleared empty Feature_Regex parameter") + elif isinstance(value, list) and value: + params['Feature_Regex'] = "|".join(str(v) for v in value if v) + print(f"[Runner] Joined Feature_Regex list: {params['Feature_Regex']}") + + return params + +# =========================================================================== +# APPLY COLUMN CONVERSION +# =========================================================================== +print("[Runner] Step 1: Converting column indices to names") +params = convert_column_indices_to_names(params, template_name) + +# =========================================================================== +# SPECIAL HANDLING FOR SPECIFIC TEMPLATES +# =========================================================================== + +# Helper function to coerce singleton lists to strings for load_csv +def _coerce_singleton_paths_for_load_csv(params, template_name): + """For load_csv templates, flatten 1-item lists to strings for path-like params.""" + if 'load_csv' not in template_name: + return params + for key in ('CSV_Files', 'CSV_Files_Configuration'): + val = params.get(key) + if isinstance(val, list) and len(val) == 1 and isinstance(val[0], (str, bytes)): + params[key] = val[0] + print(f"[Runner] Coerced {key} from list -> string") + return params + +# Special handling for String_Columns in load_csv templates +if 'load_csv' in template_name and 'String_Columns' in params: + value = params['String_Columns'] + if not isinstance(value, list): + if value in [None, "", "[]", "__ob____cb__"]: + params['String_Columns'] = [] + elif isinstance(value, str): + s = value.strip() + if s.startswith('[') and s.endswith(']'): + try: + params['String_Columns'] = json.loads(s) + except: + params['String_Columns'] = [s] if s else [] + elif ',' in s: + params['String_Columns'] = [item.strip() for item in s.split(',') if item.strip()] + else: + params['String_Columns'] = [s] if s else [] + else: + params['String_Columns'] = [] + print(f"[Runner] Ensured String_Columns is list: {params['String_Columns']}") + +# Apply coercion for load_csv files +params = _coerce_singleton_paths_for_load_csv(params, template_name) + +# Fix for Load CSV Files directory +if 'load_csv' in template_name and 'CSV_Files' in params: + # Check if csv_input_dir was created by Galaxy command + if os.path.exists('csv_input_dir') and os.path.isdir('csv_input_dir'): + params['CSV_Files'] = 'csv_input_dir' + print("[Runner] Using csv_input_dir created by Galaxy") + elif isinstance(params['CSV_Files'], str) and os.path.isfile(params['CSV_Files']): + # We have a single file path, need to get its directory + params['CSV_Files'] = os.path.dirname(params['CSV_Files']) + print(f"[Runner] Using directory of CSV file: {params['CSV_Files']}") + +# =========================================================================== +# LIST PARAMETER NORMALIZATION +# =========================================================================== +def should_normalize_as_list(key, value): + """Determine if a parameter should be normalized as a list""" + if isinstance(value, list): + return True + + if value is None or value == "": + return False + + key_lower = key.lower() + + # Skip regex parameters + if 'regex' in key_lower or 'pattern' in key_lower: + return False + + # Skip known single-value parameters + if any(x in key_lower for x in ['single', 'one', 'first', 'second', 'primary']): + return False + + # Plural forms suggest lists + if any(x in key_lower for x in ['features', 'markers', 'phenotypes', 'annotations', + 'columns', 'types', 'labels', 'regions', 'radii']): + return True + + # Check for list separators + if isinstance(value, str): + if ',' in value or '\n' in value: + return True + if value.strip().startswith('[') and value.strip().endswith(']'): + return True + + return False + +def normalize_to_list(value): + """Convert various input formats to a proper Python list""" + if value in (None, "", "All", ["All"], "all", ["all"]): + return ["All"] + + if isinstance(value, list): + return value + + if isinstance(value, str): + s = value.strip() + + # Try JSON parsing + if s.startswith('[') and s.endswith(']'): + try: + parsed = json.loads(s) + return parsed if isinstance(parsed, list) else [str(parsed)] + except: + pass + + # Split by comma + if ',' in s: + return [item.strip() for item in s.split(',') if item.strip()] + + # Split by newline + if '\n' in s: + return [item.strip() for item in s.split('\n') if item.strip()] + + # Single value + return [s] if s else [] + + return [value] if value is not None else [] + +# Normalize list parameters +print("[Runner] Step 2: Normalizing list parameters") +list_count = 0 +for key, value in list(params.items()): + if should_normalize_as_list(key, value): + original = value + normalized = normalize_to_list(value) + if original != normalized: + params[key] = normalized + list_count += 1 + if len(str(normalized)) > 100: + print(f"[Runner] Normalized {key}: {type(original).__name__} -> list of {len(normalized)} items") + else: + print(f"[Runner] Normalized {key}: {original} -> {normalized}") + +if list_count > 0: + print(f"[Runner] Normalized {list_count} list parameters") + +# CRITICAL FIX: Handle single-element lists for coordinate columns +# These should be strings, not lists +coordinate_keys = ['X_Coordinate_Column', 'Y_Coordinate_Column', 'X_centroid', 'Y_centroid'] +for key in coordinate_keys: + if key in params: + value = params[key] + if isinstance(value, list) and len(value) == 1: + params[key] = value[0] + print(f"[Runner] Extracted single value from {key}: {value} -> {params[key]}") + +# Also check for any key ending with '_Column' that has a single-element list +for key in list(params.keys()): + if key.endswith('_Column') and isinstance(params[key], list) and len(params[key]) == 1: + original = params[key] + params[key] = params[key][0] + print(f"[Runner] Extracted single value from {key}: {original} -> {params[key]}") + +# =========================================================================== +# OUTPUTS HANDLING +# =========================================================================== + +# Extract outputs specification +raw_outputs = params.pop('outputs', {}) +outputs = {} + +if isinstance(raw_outputs, dict): + outputs = raw_outputs +elif isinstance(raw_outputs, str): + try: + maybe = json.loads(_unsanitize(raw_outputs)) + if isinstance(maybe, dict): + outputs = maybe + except Exception: + pass + +if not isinstance(outputs, dict) or not outputs: + print("[Runner] Warning: 'outputs' missing or not a dict; using defaults") + if 'boxplot' in template_name or 'plot' in template_name or 'histogram' in template_name: + outputs = {'DataFrames': 'dataframe_folder', 'figures': 'figure_folder'} + elif 'load_csv' in template_name: + outputs = {'DataFrames': 'dataframe_folder'} + elif 'interactive' in template_name: + outputs = {'html': 'html_folder'} + else: + outputs = {'analysis': 'transform_output.pickle'} + +print(f"[Runner] Outputs -> {list(outputs.keys())}") + +# Create output directories +for output_type, path in outputs.items(): + if output_type != 'analysis' and path: + os.makedirs(path, exist_ok=True) + print(f"[Runner] Created {output_type} directory: {path}") + +# Add output paths to params +params['save_results'] = True + +if 'analysis' in outputs: + params['output_path'] = outputs['analysis'] + params['Output_Path'] = outputs['analysis'] + params['Output_File'] = outputs['analysis'] + +if 'DataFrames' in outputs: + df_dir = outputs['DataFrames'] + params['output_dir'] = df_dir + params['Export_Dir'] = df_dir + params['Output_File'] = os.path.join(df_dir, f'{template_name}_output.csv') + +if 'figures' in outputs: + fig_dir = outputs['figures'] + params['figure_dir'] = fig_dir + params['Figure_Dir'] = fig_dir + params['Figure_File'] = os.path.join(fig_dir, f'{template_name}.png') + +if 'html' in outputs: + html_dir = outputs['html'] + params['html_dir'] = html_dir + params['Output_File'] = os.path.join(html_dir, f'{template_name}.html') + +# Save runtime parameters +with open('params.runtime.json', 'w') as f: + json.dump(params, f, indent=2) + +# Save clean params for Galaxy display +params_display = {k: v for k, v in params.items() + if k not in ['Output_File', 'Figure_File', 'output_dir', 'figure_dir']} +with open('config_used.json', 'w') as f: + json.dump(params_display, f, indent=2) + +print(f"[Runner] Saved runtime parameters") + +# ============================================================================ +# LOAD AND EXECUTE TEMPLATE +# ============================================================================ + +# Try to import from installed package first (Docker environment) +template_module_name = template_filename.replace('.py', '') +try: + import importlib + mod = importlib.import_module(f'spac.templates.{template_module_name}') + print(f"[Runner] Loaded template from package: spac.templates.{template_module_name}") +except (ImportError, ModuleNotFoundError): + # Fallback to loading from file + print(f"[Runner] Package import failed, trying file load") + import importlib.util + + # Standard locations + template_paths = [ + f'/app/spac/templates/{template_filename}', + f'/opt/spac/templates/{template_filename}', + f'/opt/SCSAWorkflow/src/spac/templates/{template_filename}', + template_filename # Current directory + ] + + spec = None + for path in template_paths: + if os.path.exists(path): + spec = importlib.util.spec_from_file_location("template_mod", path) + if spec: + print(f"[Runner] Found template at: {path}") + break + + if not spec or not spec.loader: + print(f"[Runner] ERROR: Could not find template: {template_filename}") + sys.exit(1) + + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + +# Verify run_from_json exists +if not hasattr(mod, 'run_from_json'): + print('[Runner] ERROR: Template missing run_from_json function') + sys.exit(2) + +# Check function signature +sig = inspect.signature(mod.run_from_json) +kwargs = {} + +if 'save_results' in sig.parameters: + kwargs['save_results'] = True +if 'show_plot' in sig.parameters: + kwargs['show_plot'] = False + +print(f"[Runner] Executing template with kwargs: {kwargs}") + +# Execute template +try: + result = mod.run_from_json('params.runtime.json', **kwargs) + print(f"[Runner] Template completed, returned: {type(result).__name__}") + + # Handle different return types + if result is not None: + if isinstance(result, dict): + print(f"[Runner] Template saved files: {list(result.keys())}") + elif isinstance(result, tuple): + # Handle tuple returns + saved_count = 0 + for i, item in enumerate(result): + if hasattr(item, 'savefig') and 'figures' in outputs: + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + fig_path = os.path.join(outputs['figures'], f'figure_{i+1}.png') + item.savefig(fig_path, dpi=300, bbox_inches='tight') + plt.close(item) + saved_count += 1 + print(f"[Runner] Saved figure to {fig_path}") + elif hasattr(item, 'to_csv') and 'DataFrames' in outputs: + df_path = os.path.join(outputs['DataFrames'], f'table_{i+1}.csv') + item.to_csv(df_path, index=True) + saved_count += 1 + print(f"[Runner] Saved DataFrame to {df_path}") + + if saved_count > 0: + print(f"[Runner] Saved {saved_count} in-memory results") + + elif hasattr(result, 'to_csv') and 'DataFrames' in outputs: + df_path = os.path.join(outputs['DataFrames'], 'output.csv') + result.to_csv(df_path, index=True) + print(f"[Runner] Saved DataFrame to {df_path}") + + elif hasattr(result, 'savefig') and 'figures' in outputs: + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + fig_path = os.path.join(outputs['figures'], 'figure.png') + result.savefig(fig_path, dpi=300, bbox_inches='tight') + plt.close(result) + print(f"[Runner] Saved figure to {fig_path}") + + elif hasattr(result, 'write_h5ad') and 'analysis' in outputs: + result.write_h5ad(outputs['analysis']) + print(f"[Runner] Saved AnnData to {outputs['analysis']}") + +except Exception as e: + print(f"[Runner] ERROR in template execution: {e}") + print(f"[Runner] Error type: {type(e).__name__}") + traceback.print_exc() + + # Debug help for common issues + if "String Columns must be a *list*" in str(e): + print("\n[Runner] DEBUG: String_Columns validation failed") + print(f"[Runner] Current String_Columns value: {params.get('String_Columns')}") + print(f"[Runner] Type: {type(params.get('String_Columns'))}") + + elif "regex pattern" in str(e).lower() or "^8$" in str(e): + print("\n[Runner] DEBUG: This appears to be a column index issue") + print("[Runner] Check that column indices were properly converted to names") + print("[Runner] Current Features_to_Analyze value:", params.get('Features_to_Analyze')) + print("[Runner] Current Feature_Regex value:", params.get('Feature_Regex')) + + sys.exit(1) + +# Verify outputs +print("[Runner] Verifying outputs...") +found_outputs = False + +for output_type, path in outputs.items(): + if output_type == 'analysis': + if os.path.exists(path): + size = os.path.getsize(path) + print(f"[Runner] ✔ {output_type}: {path} ({size:,} bytes)") + found_outputs = True + else: + print(f"[Runner] ✗ {output_type}: NOT FOUND") + else: + if os.path.exists(path) and os.path.isdir(path): + files = os.listdir(path) + if files: + print(f"[Runner] ✔ {output_type}: {len(files)} files") + for f in files[:3]: + print(f"[Runner] - {f}") + if len(files) > 3: + print(f"[Runner] ... and {len(files)-3} more") + found_outputs = True + else: + print(f"[Runner] ⚠ {output_type}: directory empty") + +# Check for files in working directory and move them +print("[Runner] Checking for files in working directory...") +for file in os.listdir('.'): + if os.path.isdir(file) or file in ['params.runtime.json', 'config_used.json', + 'tool_stdout.txt', 'outputs_returned.json']: + continue + + if file.endswith('.csv') and 'DataFrames' in outputs: + if not os.path.exists(os.path.join(outputs['DataFrames'], file)): + target = os.path.join(outputs['DataFrames'], file) + shutil.move(file, target) + print(f"[Runner] Moved {file} to {target}") + found_outputs = True + elif file.endswith(('.png', '.pdf', '.jpg', '.svg')) and 'figures' in outputs: + if not os.path.exists(os.path.join(outputs['figures'], file)): + target = os.path.join(outputs['figures'], file) + shutil.move(file, target) + print(f"[Runner] Moved {file} to {target}") + found_outputs = True + +if found_outputs: + print("[Runner] === SUCCESS ===") +else: + print("[Runner] WARNING: No outputs created") + +PYTHON_RUNNER + +EXIT_CODE=$? + +if [ $EXIT_CODE -ne 0 ]; then + echo "ERROR: Template execution failed with exit code $EXIT_CODE" + exit 1 +fi + +echo "=== Execution Complete ===" +exit 0 \ No newline at end of file diff --git a/galaxy_tools/spac_zscore_normalization/spac_zscore_normalization.xml b/galaxy_tools/spac_zscore_normalization/spac_zscore_normalization.xml new file mode 100644 index 00000000..003c8089 --- /dev/null +++ b/galaxy_tools/spac_zscore_normalization/spac_zscore_normalization.xml @@ -0,0 +1,63 @@ + + Perform z-scores normalization for the selected data table in the analysis. Normalized data table... + + + nciccbr/spac:v1 + + + + python3 + + + tool_stdout.txt && + + ## Run the universal wrapper (template name without .py extension) + bash $__tool_directory__/run_spac_template.sh "$params_json" zscore_normalization + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + @misc{spac_toolkit, + author = {FNLCR DMAP Team}, + title = {SPAC: SPAtial single-Cell analysis}, + year = {2024}, + url = {https://github.com/FNLCR-DMAP/SCSAWorkflow} + } + + + \ No newline at end of file From 009d01000c6a80dad9f6dc4a508b334a19796b3b Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Tue, 7 Oct 2025 23:54:03 -0400 Subject: [PATCH 088/102] refactor: streamline galaxy tools implementation --- .../run_spac_template.sh | 793 +++++++++++----- .../spac_arcsinh_normalization.xml | 113 ++- .../spac_boxplot/run_spac_template.sh | 885 +++++++++++++----- galaxy_tools/spac_boxplot/spac_boxplot.xml | 118 ++- .../spac_boxplot_update/run_spac_template.sh | 710 -------------- .../spac_boxplot_update/spac_boxplot.xml | 84 -- .../run_spac_template.sh | 391 -------- .../spac_interactive_spatial_plot.xml | 92 -- .../spac_load_csv_files/run_spac_template.sh | 479 +++++++++- .../spac_load_csv_files.xml | 21 +- .../spac_setup_analysis/run_spac_template.sh | 229 ++++- .../spac_setup_analysis.xml | 167 ++-- .../spac_zscore_normalization.xml | 14 +- 13 files changed, 2077 insertions(+), 2019 deletions(-) delete mode 100644 galaxy_tools/spac_boxplot_update/run_spac_template.sh delete mode 100644 galaxy_tools/spac_boxplot_update/spac_boxplot.xml delete mode 100644 galaxy_tools/spac_interactive_spatial_plot/run_spac_template.sh delete mode 100644 galaxy_tools/spac_interactive_spatial_plot/spac_interactive_spatial_plot.xml diff --git a/galaxy_tools/spac_arcsinh_normalization/run_spac_template.sh b/galaxy_tools/spac_arcsinh_normalization/run_spac_template.sh index f2c32068..a93b2d6e 100644 --- a/galaxy_tools/spac_arcsinh_normalization/run_spac_template.sh +++ b/galaxy_tools/spac_arcsinh_normalization/run_spac_template.sh @@ -1,186 +1,62 @@ #!/usr/bin/env bash -# run_spac_template.sh - Docker version for Galaxy -# Version: 4.0.0 - Imports templates from installed SPAC package +# run_spac_template.sh - SPAC wrapper with column index conversion +# Version: 5.4.1 - Integrated column conversion set -euo pipefail -# Log everything to tool_stdout.txt -exec > >(tee -a tool_stdout.txt) 2>&1 - PARAMS_JSON="${1:?Missing params.json path}" -TEMPLATE_NAME="${2:?Missing template name}" # Just the name, not filename +TEMPLATE_BASE="${2:?Missing template base name}" + +# Handle both base names and full .py filenames +if [[ "$TEMPLATE_BASE" == *.py ]]; then + TEMPLATE_PY="$TEMPLATE_BASE" +elif [[ "$TEMPLATE_BASE" == "load_csv_files_with_config" ]]; then + TEMPLATE_PY="load_csv_files_with_config.py" +else + TEMPLATE_PY="${TEMPLATE_BASE}_template.py" +fi -# Use system Python inside Docker container +# Use SPAC Python environment SPAC_PYTHON="${SPAC_PYTHON:-python3}" -echo "=== SPAC Template Wrapper v4.0 (Docker) ===" +echo "=== SPAC Template Wrapper v5.3 ===" echo "Parameters: $PARAMS_JSON" -echo "Template: $TEMPLATE_NAME" +echo "Template base: $TEMPLATE_BASE" +echo "Template file: $TEMPLATE_PY" echo "Python: $SPAC_PYTHON" -echo "Working directory: $(pwd)" -# Run template through Python interpreter -"$SPAC_PYTHON" - <<'PYTHON_RUNNER' "$PARAMS_JSON" "$TEMPLATE_NAME" +# Run template through Python +"$SPAC_PYTHON" - <<'PYTHON_RUNNER' "$PARAMS_JSON" "$TEMPLATE_PY" 2>&1 | tee tool_stdout.txt import json import os import sys import copy -import importlib import traceback import inspect +import shutil +import re +import csv -# Get command line arguments +# Get arguments params_path = sys.argv[1] -template_name = sys.argv[2] # Just the name like "boxplot", not "boxplot_template.py" - -print(f"[Runner] Starting execution for template: {template_name}") -print(f"[Runner] Python version: {sys.version}") - -# ============================================================================ -# HELPER FUNCTIONS -# ============================================================================ +template_filename = sys.argv[2] -def determine_outputs_from_params(params): - """Read outputs from params if available, otherwise use defaults""" - if 'outputs' in params and isinstance(params['outputs'], dict): - outputs = params['outputs'] - print(f"[Runner] Using outputs from params: {list(outputs.keys())}") - return outputs - - # Fallback to defaults based on template name - print(f"[Runner] No outputs in params, using defaults for {template_name}") - - if template_name in ['boxplot', 'histogram', 'violin_plot', 'scatter_plot', - 'hierarchical_heatmap', 'relational_heatmap']: - return {'figures': 'figure_folder', 'DataFrames': 'dataframe_folder'} - elif template_name in ['interactive_spatial_plot', 'interactive_scatter_plot']: - return {'html': 'html_folder'} - elif template_name in ['analysis_to_csv', 'select_values']: - return {'DataFrames': 'dataframe_folder'} - elif template_name == 'setup_analysis': - return {'analysis': 'analysis_output.pickle'} - else: - return {'analysis': 'transform_output.pickle'} +print(f"[Runner] Loading parameters from: {params_path}") +print(f"[Runner] Template: {template_filename}") -def inject_output_paths(params, outputs, template_name): - """Add output paths to parameters""" - params_exec = copy.deepcopy(params) - - # Remove the 'outputs' field before execution - params_exec.pop('outputs', None) - - params_exec['save_results'] = True - - if 'analysis' in outputs: - params_exec['output_path'] = outputs['analysis'] - params_exec['Output_Path'] = outputs['analysis'] - params_exec['Output_File'] = outputs['analysis'] - - if 'DataFrames' in outputs: - df_dir = outputs['DataFrames'] - params_exec['output_dir'] = df_dir - params_exec['Export_Dir'] = df_dir - params_exec['Output_File'] = os.path.join(df_dir, f'{template_name}_output.csv') - - if 'figures' in outputs: - fig_dir = outputs['figures'] - params_exec['figure_dir'] = fig_dir - params_exec['Figure_Dir'] = fig_dir - params_exec['Figure_File'] = os.path.join(fig_dir, f'{template_name}.png') - - if 'html' in outputs: - html_dir = outputs['html'] - params_exec['output_path'] = os.path.join(html_dir, f'{template_name}.html') - params_exec['Output_File'] = params_exec['output_path'] - - return params_exec - -def handle_template_results(result, outputs, template_name): - """Save any in-memory results returned by the template""" - if result is None: - return - - saved_count = 0 - - try: - import pandas as pd - import matplotlib - matplotlib.use('Agg') - import matplotlib.pyplot as plt - - if isinstance(result, tuple): - for i, item in enumerate(result): - if hasattr(item, 'savefig') and 'figures' in outputs: - fig_path = os.path.join(outputs['figures'], f'{template_name}_{i+1}.png') - item.savefig(fig_path, dpi=300, bbox_inches='tight') - print(f"[Runner] Saved figure to {fig_path}") - plt.close(item) - saved_count += 1 - elif hasattr(item, 'to_csv') and 'DataFrames' in outputs: - csv_path = os.path.join(outputs['DataFrames'], f'{template_name}_{i+1}.csv') - item.to_csv(csv_path, index=True) - print(f"[Runner] Saved DataFrame to {csv_path}") - saved_count += 1 - - elif hasattr(result, 'to_csv') and 'DataFrames' in outputs: - csv_path = os.path.join(outputs['DataFrames'], f'{template_name}_output.csv') - result.to_csv(csv_path, index=True) - print(f"[Runner] Saved DataFrame to {csv_path}") - saved_count += 1 - - elif hasattr(result, 'savefig') and 'figures' in outputs: - fig_path = os.path.join(outputs['figures'], f'{template_name}.png') - result.savefig(fig_path, dpi=300, bbox_inches='tight') - print(f"[Runner] Saved figure to {fig_path}") - plt.close(result) - saved_count += 1 - - elif hasattr(result, 'write_h5ad') and 'analysis' in outputs: - result.write_h5ad(outputs['analysis']) - print(f"[Runner] Saved AnnData to {outputs['analysis']}") - saved_count += 1 - - except ImportError as e: - print(f"[Runner] Note: Some libraries not available for result handling: {e}") - - if saved_count > 0: - print(f"[Runner] Saved {saved_count} in-memory results") - -def verify_outputs(outputs): - """Verify that expected outputs were created""" - found_outputs = False - - for output_type, path in outputs.items(): - if output_type == 'analysis': - if os.path.exists(path): - size = os.path.getsize(path) - print(f"[Runner] ✓ {output_type}: {os.path.basename(path)} ({size:,} bytes)") - found_outputs = True - else: - print(f"[Runner] ✗ {output_type}: NOT FOUND at {path}") - else: - if os.path.exists(path) and os.path.isdir(path): - files = os.listdir(path) - if files: - total_size = sum(os.path.getsize(os.path.join(path, f)) for f in files) - print(f"[Runner] ✓ {output_type}: {len(files)} files ({total_size:,} bytes)") - for f in files[:3]: - size = os.path.getsize(os.path.join(path, f)) - print(f"[Runner] - {f} ({size:,} bytes)") - if len(files) > 3: - print(f"[Runner] ... and {len(files)-3} more files") - found_outputs = True - else: - print(f"[Runner] ⚠ {output_type}: directory exists but empty") - - if not found_outputs: - print("[Runner] WARNING: No outputs were created!") +# Load parameters +with open(params_path, 'r') as f: + params = json.load(f) -# ============================================================================ -# PARAMETER PROCESSING -# ============================================================================ +# Extract template name +template_name = os.path.basename(template_filename).replace('_template.py', '').replace('.py', '') +# =========================================================================== +# DE-SANITIZATION AND PARSING +# =========================================================================== def _unsanitize(s: str) -> str: """Remove Galaxy's parameter sanitization tokens""" + if not isinstance(s, str): + return s replacements = { '__ob__': '[', '__cb__': ']', '__oc__': '{', '__cc__': '}', @@ -188,21 +64,20 @@ def _unsanitize(s: str) -> str: '__gt__': '>', '__lt__': '<', '__cn__': '\n', '__cr__': '\r', '__tc__': '\t', '__pd__': '#', - '__at__': '@', '__cm__': ',', - '__dollar__': '$', '__us__': '_' + '__at__': '@', '__cm__': ',' } for token, char in replacements.items(): s = s.replace(token, char) return s def _maybe_parse(v): - """Recursively unsanitize and parse JSON where possible""" + """Recursively de-sanitize and JSON-parse strings where possible.""" if isinstance(v, str): u = _unsanitize(v).strip() if (u.startswith('[') and u.endswith(']')) or (u.startswith('{') and u.endswith('}')): try: return json.loads(u) - except: + except Exception: return u return u elif isinstance(v, dict): @@ -211,6 +86,272 @@ def _maybe_parse(v): return [_maybe_parse(item) for item in v] return v +# Normalize the whole params tree +params = _maybe_parse(params) + +# =========================================================================== +# COLUMN INDEX CONVERSION - CRITICAL FOR SETUP ANALYSIS +# =========================================================================== +def should_skip_column_conversion(template_name): + """Some templates don't need column index conversion""" + return 'load_csv' in template_name + +def read_file_headers(filepath): + """Read column headers from various file formats""" + try: + import pandas as pd + + # Try pandas auto-detect + try: + df = pd.read_csv(filepath, nrows=1) + if len(df.columns) > 1 or not df.columns[0].startswith('Unnamed'): + columns = df.columns.tolist() + print(f"[Runner] Pandas auto-detected delimiter, found {len(columns)} columns") + return columns + except: + pass + + # Try common delimiters + for sep in ['\t', ',', ';', '|', ' ']: + try: + df = pd.read_csv(filepath, sep=sep, nrows=1) + if len(df.columns) > 1: + columns = df.columns.tolist() + sep_name = {'\t': 'tab', ',': 'comma', ';': 'semicolon', + '|': 'pipe', ' ': 'space'}.get(sep, sep) + print(f"[Runner] Pandas found {sep_name}-delimited file with {len(columns)} columns") + return columns + except: + continue + except ImportError: + print("[Runner] pandas not available, using csv fallback") + + # CSV module fallback + try: + with open(filepath, 'r', encoding='utf-8', errors='replace', newline='') as f: + sample = f.read(8192) + f.seek(0) + + try: + dialect = csv.Sniffer().sniff(sample, delimiters='\t,;| ') + reader = csv.reader(f, dialect) + header = next(reader) + columns = [h.strip().strip('"') for h in header if h.strip()] + if columns: + print(f"[Runner] csv.Sniffer detected {len(columns)} columns") + return columns + except: + f.seek(0) + first_line = f.readline().strip() + for sep in ['\t', ',', ';', '|']: + if sep in first_line: + columns = [h.strip().strip('"') for h in first_line.split(sep)] + if len(columns) > 1: + print(f"[Runner] Manual parsing found {len(columns)} columns") + return columns + except Exception as e: + print(f"[Runner] Failed to read headers: {e}") + + return None + +def should_convert_param(key, value): + """Check if parameter contains column indices""" + if value is None or value == "" or value == [] or value == {}: + return False + + key_lower = key.lower() + + # Skip String_Columns - it's names not indices + if key == 'String_Columns': + return False + + # Skip output/path parameters + if any(x in key_lower for x in ['output', 'path', 'file', 'directory', 'save', 'export']): + return False + + # Skip regex/pattern parameters (but we'll handle Feature_Regex specially) + if 'regex' in key_lower or 'pattern' in key_lower: + return False + + # Parameters with 'column' likely have indices + if 'column' in key_lower or '_col' in key_lower: + return True + + # Known index parameters + if key in {'Annotation_s_', 'Features_to_Analyze', 'Features', 'Markers', 'Markers_to_Plot', 'Phenotypes'}: + return True + + # Check if values look like indices + if isinstance(value, list): + return all(isinstance(v, int) or (isinstance(v, str) and v.strip().isdigit()) for v in value if v) + elif isinstance(value, (int, str)): + return isinstance(value, int) or (isinstance(value, str) and value.strip().isdigit()) + + return False + +def convert_single_index(item, columns): + """Convert a single column index to name""" + if isinstance(item, str) and not item.strip().isdigit(): + return item + + try: + if isinstance(item, str): + item = int(item.strip()) + elif isinstance(item, float): + item = int(item) + except (ValueError, AttributeError): + return item + + if isinstance(item, int): + idx = item - 1 # Galaxy uses 1-based indexing + if 0 <= idx < len(columns): + return columns[idx] + elif 0 <= item < len(columns): # Fallback for 0-based + print(f"[Runner] Note: Found 0-based index {item}, converting to {columns[item]}") + return columns[item] + else: + print(f"[Runner] Warning: Index {item} out of range (have {len(columns)} columns)") + + return item + +def convert_column_indices_to_names(params, template_name): + """Convert column indices to names for templates that need it""" + + if should_skip_column_conversion(template_name): + print(f"[Runner] Skipping column conversion for {template_name}") + return params + + print(f"[Runner] Checking for column index conversion (template: {template_name})") + + # Find input file + input_file = None + input_keys = ['Upstream_Dataset', 'Upstream_Analysis', 'CSV_Files', + 'Input_File', 'Input_Dataset', 'Data_File'] + + for key in input_keys: + if key in params: + value = params[key] + if isinstance(value, list) and value: + value = value[0] + if value and os.path.exists(str(value)): + input_file = str(value) + print(f"[Runner] Found input file via {key}: {os.path.basename(input_file)}") + break + + if not input_file: + print("[Runner] No input file found for column conversion") + return params + + # Read headers + columns = read_file_headers(input_file) + if not columns: + print("[Runner] Could not read column headers, skipping conversion") + return params + + print(f"[Runner] Successfully read {len(columns)} columns") + if len(columns) <= 10: + print(f"[Runner] Columns: {columns}") + else: + print(f"[Runner] First 10 columns: {columns[:10]}") + + # Convert indices to names + converted_count = 0 + for key, value in params.items(): + # Skip non-column parameters + if not should_convert_param(key, value): + continue + + # Convert indices + if isinstance(value, list): + converted_items = [] + for item in value: + converted = convert_single_index(item, columns) + if converted is not None: + converted_items.append(converted) + converted_value = converted_items + else: + converted_value = convert_single_index(value, columns) + + if value != converted_value: + params[key] = converted_value + converted_count += 1 + print(f"[Runner] Converted {key}: {value} -> {converted_value}") + + if converted_count > 0: + print(f"[Runner] Total conversions: {converted_count} parameters") + + # CRITICAL: Handle Feature_Regex specially + if 'Feature_Regex' in params: + value = params['Feature_Regex'] + if value in [[], [""], "__ob____cb__", "[]", "", None]: + params['Feature_Regex'] = "" + print("[Runner] Cleared empty Feature_Regex parameter") + elif isinstance(value, list) and value: + params['Feature_Regex'] = "|".join(str(v) for v in value if v) + print(f"[Runner] Joined Feature_Regex list: {params['Feature_Regex']}") + + return params + +# =========================================================================== +# APPLY COLUMN CONVERSION +# =========================================================================== +print("[Runner] Step 1: Converting column indices to names") +params = convert_column_indices_to_names(params, template_name) + +# =========================================================================== +# SPECIAL HANDLING FOR SPECIFIC TEMPLATES +# =========================================================================== + +# Helper function to coerce singleton lists to strings for load_csv +def _coerce_singleton_paths_for_load_csv(params, template_name): + """For load_csv templates, flatten 1-item lists to strings for path-like params.""" + if 'load_csv' not in template_name: + return params + for key in ('CSV_Files', 'CSV_Files_Configuration'): + val = params.get(key) + if isinstance(val, list) and len(val) == 1 and isinstance(val[0], (str, bytes)): + params[key] = val[0] + print(f"[Runner] Coerced {key} from list -> string") + return params + +# Special handling for String_Columns in load_csv templates +if 'load_csv' in template_name and 'String_Columns' in params: + value = params['String_Columns'] + if not isinstance(value, list): + if value in [None, "", "[]", "__ob____cb__"]: + params['String_Columns'] = [] + elif isinstance(value, str): + s = value.strip() + if s.startswith('[') and s.endswith(']'): + try: + params['String_Columns'] = json.loads(s) + except: + params['String_Columns'] = [s] if s else [] + elif ',' in s: + params['String_Columns'] = [item.strip() for item in s.split(',') if item.strip()] + else: + params['String_Columns'] = [s] if s else [] + else: + params['String_Columns'] = [] + print(f"[Runner] Ensured String_Columns is list: {params['String_Columns']}") + +# Apply coercion for load_csv files +params = _coerce_singleton_paths_for_load_csv(params, template_name) + +# Fix for Load CSV Files directory +if 'load_csv' in template_name and 'CSV_Files' in params: + # Check if csv_input_dir was created by Galaxy command + if os.path.exists('csv_input_dir') and os.path.isdir('csv_input_dir'): + params['CSV_Files'] = 'csv_input_dir' + print("[Runner] Using csv_input_dir created by Galaxy") + elif isinstance(params['CSV_Files'], str) and os.path.isfile(params['CSV_Files']): + # We have a single file path, need to get its directory + params['CSV_Files'] = os.path.dirname(params['CSV_Files']) + print(f"[Runner] Using directory of CSV file: {params['CSV_Files']}") + +# =========================================================================== +# LIST PARAMETER NORMALIZATION +# =========================================================================== def should_normalize_as_list(key, value): """Determine if a parameter should be normalized as a list""" if isinstance(value, list): @@ -221,16 +362,20 @@ def should_normalize_as_list(key, value): key_lower = key.lower() + # Skip regex parameters if 'regex' in key_lower or 'pattern' in key_lower: return False + # Skip known single-value parameters if any(x in key_lower for x in ['single', 'one', 'first', 'second', 'primary']): return False + # Plural forms suggest lists if any(x in key_lower for x in ['features', 'markers', 'phenotypes', 'annotations', 'columns', 'types', 'labels', 'regions', 'radii']): return True + # Check for list separators if isinstance(value, str): if ',' in value or '\n' in value: return True @@ -250,6 +395,7 @@ def normalize_to_list(value): if isinstance(value, str): s = value.strip() + # Try JSON parsing if s.startswith('[') and s.endswith(']'): try: parsed = json.loads(s) @@ -257,135 +403,308 @@ def normalize_to_list(value): except: pass + # Split by comma if ',' in s: return [item.strip() for item in s.split(',') if item.strip()] + # Split by newline if '\n' in s: return [item.strip() for item in s.split('\n') if item.strip()] + # Single value return [s] if s else [] return [value] if value is not None else [] -# ============================================================================ -# MAIN EXECUTION -# ============================================================================ - -# Load parameters -with open(params_path, 'r') as f: - params = json.load(f) - print(f"[Runner] Loaded {len(params)} parameters from {params_path}") - -# Step 1: De-sanitize Galaxy parameters -print("[Runner] Step 1: De-sanitizing Galaxy parameters") -params = _maybe_parse(params) - -# Step 2: Get outputs from params (injected by Galaxy) -print("[Runner] Step 2: Getting output structure") -outputs = determine_outputs_from_params(params) - -# Create output directories -for output_type, path in outputs.items(): - if output_type != 'analysis' and path: - os.makedirs(path, exist_ok=True) - print(f"[Runner] Created {output_type} directory: {path}") - -# Step 3: Normalize list parameters -print("[Runner] Step 3: Normalizing list parameters") +# Normalize list parameters +print("[Runner] Step 2: Normalizing list parameters") list_count = 0 for key, value in list(params.items()): - if key != 'outputs' and should_normalize_as_list(key, value): + if should_normalize_as_list(key, value): original = value normalized = normalize_to_list(value) if original != normalized: params[key] = normalized list_count += 1 - print(f"[Runner] Normalized {key}: {original} -> {normalized}") + if len(str(normalized)) > 100: + print(f"[Runner] Normalized {key}: {type(original).__name__} -> list of {len(normalized)} items") + else: + print(f"[Runner] Normalized {key}: {original} -> {normalized}") if list_count > 0: print(f"[Runner] Normalized {list_count} list parameters") -# Step 4: Inject output paths -print("[Runner] Step 4: Injecting output paths") -params_exec = inject_output_paths(params, outputs, template_name) +# CRITICAL FIX: Handle single-element lists for coordinate columns +# These should be strings, not lists +coordinate_keys = ['X_Coordinate_Column', 'Y_Coordinate_Column', 'X_centroid', 'Y_centroid'] +for key in coordinate_keys: + if key in params: + value = params[key] + if isinstance(value, list) and len(value) == 1: + params[key] = value[0] + print(f"[Runner] Extracted single value from {key}: {value} -> {params[key]}") + +# Also check for any key ending with '_Column' that has a single-element list +for key in list(params.keys()): + if key.endswith('_Column') and isinstance(params[key], list) and len(params[key]) == 1: + original = params[key] + params[key] = params[key][0] + print(f"[Runner] Extracted single value from {key}: {original} -> {params[key]}") + +# =========================================================================== +# OUTPUTS HANDLING +# =========================================================================== + +# Extract outputs specification +raw_outputs = params.pop('outputs', {}) +outputs = {} + +if isinstance(raw_outputs, dict): + outputs = raw_outputs +elif isinstance(raw_outputs, str): + try: + maybe = json.loads(_unsanitize(raw_outputs)) + if isinstance(maybe, dict): + outputs = maybe + except Exception: + pass -# Save parameter files -with open('params.exec.json', 'w') as f: - json.dump(params_exec, f, indent=2) +if not isinstance(outputs, dict) or not outputs: + print("[Runner] Warning: 'outputs' missing or not a dict; using defaults") + if 'boxplot' in template_name or 'plot' in template_name or 'histogram' in template_name: + outputs = {'DataFrames': 'dataframe_folder', 'figures': 'figure_folder'} + elif 'load_csv' in template_name: + outputs = {'DataFrames': 'dataframe_folder'} + elif 'interactive' in template_name: + outputs = {'html': 'html_folder'} + else: + outputs = {'analysis': 'transform_output.pickle'} +print(f"[Runner] Outputs -> {list(outputs.keys())}") + +# Create output directories +for output_type, path in outputs.items(): + if output_type != 'analysis' and path: + os.makedirs(path, exist_ok=True) + print(f"[Runner] Created {output_type} directory: {path}") + +# Add output paths to params +params['save_results'] = True + +if 'analysis' in outputs: + params['output_path'] = outputs['analysis'] + params['Output_Path'] = outputs['analysis'] + params['Output_File'] = outputs['analysis'] + +if 'DataFrames' in outputs: + df_dir = outputs['DataFrames'] + params['output_dir'] = df_dir + params['Export_Dir'] = df_dir + params['Output_File'] = os.path.join(df_dir, f'{template_name}_output.csv') + +if 'figures' in outputs: + fig_dir = outputs['figures'] + params['figure_dir'] = fig_dir + params['Figure_Dir'] = fig_dir + params['Figure_File'] = os.path.join(fig_dir, f'{template_name}.png') + +if 'html' in outputs: + html_dir = outputs['html'] + params['html_dir'] = html_dir + params['Output_File'] = os.path.join(html_dir, f'{template_name}.html') + +# Save runtime parameters +with open('params.runtime.json', 'w') as f: + json.dump(params, f, indent=2) + +# Save clean params for Galaxy display +params_display = {k: v for k, v in params.items() + if k not in ['Output_File', 'Figure_File', 'output_dir', 'figure_dir']} with open('config_used.json', 'w') as f: - params_display = {k: v for k, v in params.items() - if k != 'outputs' and - not any(x in k.lower() for x in ['output', 'save', 'path', 'dir', 'file', 'export'])} json.dump(params_display, f, indent=2) -print(f"[Runner] Saved params.exec.json ({len(params_exec)} parameters)") -print(f"[Runner] Saved config_used.json ({len(params_display)} display parameters)") +print(f"[Runner] Saved runtime parameters") -# Step 5: Load template module from installed SPAC package -print(f"[Runner] Step 5: Loading template '{template_name}' from SPAC package") +# ============================================================================ +# LOAD AND EXECUTE TEMPLATE +# ============================================================================ +# Try to import from installed package first (Docker environment) +template_module_name = template_filename.replace('.py', '') try: - # Import from spac.templates package - module_name = f'spac.templates.{template_name}_template' - mod = importlib.import_module(module_name) - print(f"[Runner] Successfully imported {module_name}") -except ImportError as e: - print(f"[Runner] ERROR: Could not import {module_name}: {e}") - print(f"[Runner] Available modules in spac.templates:") - try: - import spac.templates - import pkgutil - for importer, modname, ispkg in pkgutil.iter_modules(spac.templates.__path__): - print(f"[Runner] - {modname}") - except: - pass - sys.exit(1) + import importlib + mod = importlib.import_module(f'spac.templates.{template_module_name}') + print(f"[Runner] Loaded template from package: spac.templates.{template_module_name}") +except (ImportError, ModuleNotFoundError): + # Fallback to loading from file + print(f"[Runner] Package import failed, trying file load") + import importlib.util + + # Standard locations + template_paths = [ + f'/app/spac/templates/{template_filename}', + f'/opt/spac/templates/{template_filename}', + f'/opt/SCSAWorkflow/src/spac/templates/{template_filename}', + template_filename # Current directory + ] + + spec = None + for path in template_paths: + if os.path.exists(path): + spec = importlib.util.spec_from_file_location("template_mod", path) + if spec: + print(f"[Runner] Found template at: {path}") + break + + if not spec or not spec.loader: + print(f"[Runner] ERROR: Could not find template: {template_filename}") + sys.exit(1) + + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) +# Verify run_from_json exists if not hasattr(mod, 'run_from_json'): print('[Runner] ERROR: Template missing run_from_json function') sys.exit(2) -# Step 6: Execute template -print("[Runner] Step 6: Executing template") +# Check function signature sig = inspect.signature(mod.run_from_json) kwargs = {} if 'save_results' in sig.parameters: kwargs['save_results'] = True - print("[Runner] Added save_results=True to kwargs") - if 'show_plot' in sig.parameters: kwargs['show_plot'] = False - print("[Runner] Added show_plot=False to kwargs") +print(f"[Runner] Executing template with kwargs: {kwargs}") + +# Execute template try: - print(f"[Runner] Calling run_from_json('params.exec.json', **{kwargs})") - result = mod.run_from_json('params.exec.json', **kwargs) - print(f"[Runner] Template completed successfully, returned {type(result).__name__}") + result = mod.run_from_json('params.runtime.json', **kwargs) + print(f"[Runner] Template completed, returned: {type(result).__name__}") + + # Handle different return types + if result is not None: + if isinstance(result, dict): + print(f"[Runner] Template saved files: {list(result.keys())}") + elif isinstance(result, tuple): + # Handle tuple returns + saved_count = 0 + for i, item in enumerate(result): + if hasattr(item, 'savefig') and 'figures' in outputs: + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + fig_path = os.path.join(outputs['figures'], f'figure_{i+1}.png') + item.savefig(fig_path, dpi=300, bbox_inches='tight') + plt.close(item) + saved_count += 1 + print(f"[Runner] Saved figure to {fig_path}") + elif hasattr(item, 'to_csv') and 'DataFrames' in outputs: + df_path = os.path.join(outputs['DataFrames'], f'table_{i+1}.csv') + item.to_csv(df_path, index=True) + saved_count += 1 + print(f"[Runner] Saved DataFrame to {df_path}") + + if saved_count > 0: + print(f"[Runner] Saved {saved_count} in-memory results") + + elif hasattr(result, 'to_csv') and 'DataFrames' in outputs: + df_path = os.path.join(outputs['DataFrames'], 'output.csv') + result.to_csv(df_path, index=True) + print(f"[Runner] Saved DataFrame to {df_path}") + + elif hasattr(result, 'savefig') and 'figures' in outputs: + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + fig_path = os.path.join(outputs['figures'], 'figure.png') + result.savefig(fig_path, dpi=300, bbox_inches='tight') + plt.close(result) + print(f"[Runner] Saved figure to {fig_path}") + + elif hasattr(result, 'write_h5ad') and 'analysis' in outputs: + result.write_h5ad(outputs['analysis']) + print(f"[Runner] Saved AnnData to {outputs['analysis']}") + except Exception as e: print(f"[Runner] ERROR in template execution: {e}") print(f"[Runner] Error type: {type(e).__name__}") traceback.print_exc() + + # Debug help for common issues + if "String Columns must be a *list*" in str(e): + print("\n[Runner] DEBUG: String_Columns validation failed") + print(f"[Runner] Current String_Columns value: {params.get('String_Columns')}") + print(f"[Runner] Type: {type(params.get('String_Columns'))}") + + elif "regex pattern" in str(e).lower() or "^8$" in str(e): + print("\n[Runner] DEBUG: This appears to be a column index issue") + print("[Runner] Check that column indices were properly converted to names") + print("[Runner] Current Features_to_Analyze value:", params.get('Features_to_Analyze')) + print("[Runner] Current Feature_Regex value:", params.get('Feature_Regex')) + sys.exit(1) -# Handle any returned objects -handle_template_results(result, outputs, template_name) - -# Step 7: Verify outputs -print("[Runner] Step 7: Verifying outputs") -verify_outputs(outputs) +# Verify outputs +print("[Runner] Verifying outputs...") +found_outputs = False -print("[Runner] === Execution completed successfully ===") +for output_type, path in outputs.items(): + if output_type == 'analysis': + if os.path.exists(path): + size = os.path.getsize(path) + print(f"[Runner] ✔ {output_type}: {path} ({size:,} bytes)") + found_outputs = True + else: + print(f"[Runner] ✗ {output_type}: NOT FOUND") + else: + if os.path.exists(path) and os.path.isdir(path): + files = os.listdir(path) + if files: + print(f"[Runner] ✔ {output_type}: {len(files)} files") + for f in files[:3]: + print(f"[Runner] - {f}") + if len(files) > 3: + print(f"[Runner] ... and {len(files)-3} more") + found_outputs = True + else: + print(f"[Runner] ⚠ {output_type}: directory empty") + +# Check for files in working directory and move them +print("[Runner] Checking for files in working directory...") +for file in os.listdir('.'): + if os.path.isdir(file) or file in ['params.runtime.json', 'config_used.json', + 'tool_stdout.txt', 'outputs_returned.json']: + continue + + if file.endswith('.csv') and 'DataFrames' in outputs: + if not os.path.exists(os.path.join(outputs['DataFrames'], file)): + target = os.path.join(outputs['DataFrames'], file) + shutil.move(file, target) + print(f"[Runner] Moved {file} to {target}") + found_outputs = True + elif file.endswith(('.png', '.pdf', '.jpg', '.svg')) and 'figures' in outputs: + if not os.path.exists(os.path.join(outputs['figures'], file)): + target = os.path.join(outputs['figures'], file) + shutil.move(file, target) + print(f"[Runner] Moved {file} to {target}") + found_outputs = True + +if found_outputs: + print("[Runner] === SUCCESS ===") +else: + print("[Runner] WARNING: No outputs created") PYTHON_RUNNER EXIT_CODE=$? if [ $EXIT_CODE -ne 0 ]; then - echo "" - echo "=== Template execution failed with exit code $EXIT_CODE ===" - echo "" + echo "ERROR: Template execution failed with exit code $EXIT_CODE" + exit 1 fi -exit $EXIT_CODE \ No newline at end of file +echo "=== Execution Complete ===" +exit 0 \ No newline at end of file diff --git a/galaxy_tools/spac_arcsinh_normalization/spac_arcsinh_normalization.xml b/galaxy_tools/spac_arcsinh_normalization/spac_arcsinh_normalization.xml index 738a7432..ad0f4baf 100644 --- a/galaxy_tools/spac_arcsinh_normalization/spac_arcsinh_normalization.xml +++ b/galaxy_tools/spac_arcsinh_normalization/spac_arcsinh_normalization.xml @@ -1,47 +1,70 @@ - - Normalize features either by a user-defined co-factor or a determined percentile. - - - nciccbr/spac:v1 - - - - python3 - - - tool_stdout.txt && - - ## Run the universal wrapper - bash $__tool_directory__/run_spac_template.sh "$params_json" arcsinh_normalization - ]]> - - - - - - - + + Normalize features either by a user-defined co-factor or a determined percentile, allowing for ef... + + + nciccbr/spac:v1 + + + + python3 + + + tool_stdout.txt && + + ## Run the universal wrapper (template name without .py extension) + bash $__tool_directory__/run_spac_template.sh "$params_json" arcsinh_normalization + ]]> + + + + + + + + - - + + - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + @misc{spac_toolkit, + author = {FNLCR DMAP Team}, + title = {SPAC: SPAtial single-Cell analysis}, + year = {2024}, + url = {https://github.com/FNLCR-DMAP/SCSAWorkflow} + } + + + \ No newline at end of file diff --git a/galaxy_tools/spac_boxplot/run_spac_template.sh b/galaxy_tools/spac_boxplot/run_spac_template.sh index f2c32068..5e08ae50 100644 --- a/galaxy_tools/spac_boxplot/run_spac_template.sh +++ b/galaxy_tools/spac_boxplot/run_spac_template.sh @@ -1,186 +1,62 @@ #!/usr/bin/env bash -# run_spac_template.sh - Docker version for Galaxy -# Version: 4.0.0 - Imports templates from installed SPAC package +# run_spac_template.sh - SPAC wrapper with column index conversion +# Version: 5.4.2 - Integrated column conversion set -euo pipefail -# Log everything to tool_stdout.txt -exec > >(tee -a tool_stdout.txt) 2>&1 - PARAMS_JSON="${1:?Missing params.json path}" -TEMPLATE_NAME="${2:?Missing template name}" # Just the name, not filename +TEMPLATE_BASE="${2:?Missing template base name}" + +# Handle both base names and full .py filenames +if [[ "$TEMPLATE_BASE" == *.py ]]; then + TEMPLATE_PY="$TEMPLATE_BASE" +elif [[ "$TEMPLATE_BASE" == "load_csv_files_with_config" ]]; then + TEMPLATE_PY="load_csv_files_with_config.py" +else + TEMPLATE_PY="${TEMPLATE_BASE}_template.py" +fi -# Use system Python inside Docker container +# Use SPAC Python environment SPAC_PYTHON="${SPAC_PYTHON:-python3}" -echo "=== SPAC Template Wrapper v4.0 (Docker) ===" +echo "=== SPAC Template Wrapper v5.4 ===" echo "Parameters: $PARAMS_JSON" -echo "Template: $TEMPLATE_NAME" +echo "Template base: $TEMPLATE_BASE" +echo "Template file: $TEMPLATE_PY" echo "Python: $SPAC_PYTHON" -echo "Working directory: $(pwd)" -# Run template through Python interpreter -"$SPAC_PYTHON" - <<'PYTHON_RUNNER' "$PARAMS_JSON" "$TEMPLATE_NAME" +# Run template through Python +"$SPAC_PYTHON" - <<'PYTHON_RUNNER' "$PARAMS_JSON" "$TEMPLATE_PY" 2>&1 | tee tool_stdout.txt import json import os import sys import copy -import importlib import traceback import inspect +import shutil +import re +import csv -# Get command line arguments +# Get arguments params_path = sys.argv[1] -template_name = sys.argv[2] # Just the name like "boxplot", not "boxplot_template.py" +template_filename = sys.argv[2] -print(f"[Runner] Starting execution for template: {template_name}") -print(f"[Runner] Python version: {sys.version}") - -# ============================================================================ -# HELPER FUNCTIONS -# ============================================================================ - -def determine_outputs_from_params(params): - """Read outputs from params if available, otherwise use defaults""" - if 'outputs' in params and isinstance(params['outputs'], dict): - outputs = params['outputs'] - print(f"[Runner] Using outputs from params: {list(outputs.keys())}") - return outputs - - # Fallback to defaults based on template name - print(f"[Runner] No outputs in params, using defaults for {template_name}") - - if template_name in ['boxplot', 'histogram', 'violin_plot', 'scatter_plot', - 'hierarchical_heatmap', 'relational_heatmap']: - return {'figures': 'figure_folder', 'DataFrames': 'dataframe_folder'} - elif template_name in ['interactive_spatial_plot', 'interactive_scatter_plot']: - return {'html': 'html_folder'} - elif template_name in ['analysis_to_csv', 'select_values']: - return {'DataFrames': 'dataframe_folder'} - elif template_name == 'setup_analysis': - return {'analysis': 'analysis_output.pickle'} - else: - return {'analysis': 'transform_output.pickle'} - -def inject_output_paths(params, outputs, template_name): - """Add output paths to parameters""" - params_exec = copy.deepcopy(params) - - # Remove the 'outputs' field before execution - params_exec.pop('outputs', None) - - params_exec['save_results'] = True - - if 'analysis' in outputs: - params_exec['output_path'] = outputs['analysis'] - params_exec['Output_Path'] = outputs['analysis'] - params_exec['Output_File'] = outputs['analysis'] - - if 'DataFrames' in outputs: - df_dir = outputs['DataFrames'] - params_exec['output_dir'] = df_dir - params_exec['Export_Dir'] = df_dir - params_exec['Output_File'] = os.path.join(df_dir, f'{template_name}_output.csv') - - if 'figures' in outputs: - fig_dir = outputs['figures'] - params_exec['figure_dir'] = fig_dir - params_exec['Figure_Dir'] = fig_dir - params_exec['Figure_File'] = os.path.join(fig_dir, f'{template_name}.png') - - if 'html' in outputs: - html_dir = outputs['html'] - params_exec['output_path'] = os.path.join(html_dir, f'{template_name}.html') - params_exec['Output_File'] = params_exec['output_path'] - - return params_exec +print(f"[Runner] Loading parameters from: {params_path}") +print(f"[Runner] Template: {template_filename}") -def handle_template_results(result, outputs, template_name): - """Save any in-memory results returned by the template""" - if result is None: - return - - saved_count = 0 - - try: - import pandas as pd - import matplotlib - matplotlib.use('Agg') - import matplotlib.pyplot as plt - - if isinstance(result, tuple): - for i, item in enumerate(result): - if hasattr(item, 'savefig') and 'figures' in outputs: - fig_path = os.path.join(outputs['figures'], f'{template_name}_{i+1}.png') - item.savefig(fig_path, dpi=300, bbox_inches='tight') - print(f"[Runner] Saved figure to {fig_path}") - plt.close(item) - saved_count += 1 - elif hasattr(item, 'to_csv') and 'DataFrames' in outputs: - csv_path = os.path.join(outputs['DataFrames'], f'{template_name}_{i+1}.csv') - item.to_csv(csv_path, index=True) - print(f"[Runner] Saved DataFrame to {csv_path}") - saved_count += 1 - - elif hasattr(result, 'to_csv') and 'DataFrames' in outputs: - csv_path = os.path.join(outputs['DataFrames'], f'{template_name}_output.csv') - result.to_csv(csv_path, index=True) - print(f"[Runner] Saved DataFrame to {csv_path}") - saved_count += 1 - - elif hasattr(result, 'savefig') and 'figures' in outputs: - fig_path = os.path.join(outputs['figures'], f'{template_name}.png') - result.savefig(fig_path, dpi=300, bbox_inches='tight') - print(f"[Runner] Saved figure to {fig_path}") - plt.close(result) - saved_count += 1 - - elif hasattr(result, 'write_h5ad') and 'analysis' in outputs: - result.write_h5ad(outputs['analysis']) - print(f"[Runner] Saved AnnData to {outputs['analysis']}") - saved_count += 1 - - except ImportError as e: - print(f"[Runner] Note: Some libraries not available for result handling: {e}") - - if saved_count > 0: - print(f"[Runner] Saved {saved_count} in-memory results") - -def verify_outputs(outputs): - """Verify that expected outputs were created""" - found_outputs = False - - for output_type, path in outputs.items(): - if output_type == 'analysis': - if os.path.exists(path): - size = os.path.getsize(path) - print(f"[Runner] ✓ {output_type}: {os.path.basename(path)} ({size:,} bytes)") - found_outputs = True - else: - print(f"[Runner] ✗ {output_type}: NOT FOUND at {path}") - else: - if os.path.exists(path) and os.path.isdir(path): - files = os.listdir(path) - if files: - total_size = sum(os.path.getsize(os.path.join(path, f)) for f in files) - print(f"[Runner] ✓ {output_type}: {len(files)} files ({total_size:,} bytes)") - for f in files[:3]: - size = os.path.getsize(os.path.join(path, f)) - print(f"[Runner] - {f} ({size:,} bytes)") - if len(files) > 3: - print(f"[Runner] ... and {len(files)-3} more files") - found_outputs = True - else: - print(f"[Runner] ⚠ {output_type}: directory exists but empty") - - if not found_outputs: - print("[Runner] WARNING: No outputs were created!") +# Load parameters +with open(params_path, 'r') as f: + params = json.load(f) -# ============================================================================ -# PARAMETER PROCESSING -# ============================================================================ +# Extract template name +template_name = os.path.basename(template_filename).replace('_template.py', '').replace('.py', '') +# =========================================================================== +# DE-SANITIZATION AND PARSING +# =========================================================================== def _unsanitize(s: str) -> str: """Remove Galaxy's parameter sanitization tokens""" + if not isinstance(s, str): + return s replacements = { '__ob__': '[', '__cb__': ']', '__oc__': '{', '__cc__': '}', @@ -188,21 +64,20 @@ def _unsanitize(s: str) -> str: '__gt__': '>', '__lt__': '<', '__cn__': '\n', '__cr__': '\r', '__tc__': '\t', '__pd__': '#', - '__at__': '@', '__cm__': ',', - '__dollar__': '$', '__us__': '_' + '__at__': '@', '__cm__': ',' } for token, char in replacements.items(): s = s.replace(token, char) return s def _maybe_parse(v): - """Recursively unsanitize and parse JSON where possible""" + """Recursively de-sanitize and JSON-parse strings where possible.""" if isinstance(v, str): u = _unsanitize(v).strip() if (u.startswith('[') and u.endswith(']')) or (u.startswith('{') and u.endswith('}')): try: return json.loads(u) - except: + except Exception: return u return u elif isinstance(v, dict): @@ -211,45 +86,370 @@ def _maybe_parse(v): return [_maybe_parse(item) for item in v] return v +# Normalize the whole params tree +params = _maybe_parse(params) + +# =========================================================================== +# COLUMN INDEX CONVERSION - CRITICAL FOR SETUP ANALYSIS +# =========================================================================== +def should_skip_column_conversion(template_name): + """Some templates don't need column index conversion""" + return 'load_csv' in template_name + +def read_file_headers(filepath): + """Read column headers from various file formats""" + try: + import pandas as pd + + # Try pandas auto-detect + try: + df = pd.read_csv(filepath, nrows=1) + if len(df.columns) > 1 or not df.columns[0].startswith('Unnamed'): + columns = df.columns.tolist() + print(f"[Runner] Pandas auto-detected delimiter, found {len(columns)} columns") + return columns + except: + pass + + # Try common delimiters + for sep in ['\t', ',', ';', '|', ' ']: + try: + df = pd.read_csv(filepath, sep=sep, nrows=1) + if len(df.columns) > 1: + columns = df.columns.tolist() + sep_name = {'\t': 'tab', ',': 'comma', ';': 'semicolon', + '|': 'pipe', ' ': 'space'}.get(sep, sep) + print(f"[Runner] Pandas found {sep_name}-delimited file with {len(columns)} columns") + return columns + except: + continue + except ImportError: + print("[Runner] pandas not available, using csv fallback") + + # CSV module fallback + try: + with open(filepath, 'r', encoding='utf-8', errors='replace', newline='') as f: + sample = f.read(8192) + f.seek(0) + + try: + dialect = csv.Sniffer().sniff(sample, delimiters='\t,;| ') + reader = csv.reader(f, dialect) + header = next(reader) + columns = [h.strip().strip('"') for h in header if h.strip()] + if columns: + print(f"[Runner] csv.Sniffer detected {len(columns)} columns") + return columns + except: + f.seek(0) + first_line = f.readline().strip() + for sep in ['\t', ',', ';', '|']: + if sep in first_line: + columns = [h.strip().strip('"') for h in first_line.split(sep)] + if len(columns) > 1: + print(f"[Runner] Manual parsing found {len(columns)} columns") + return columns + except Exception as e: + print(f"[Runner] Failed to read headers: {e}") + + return None + +def should_convert_param(key, value): + """Check if parameter contains column indices""" + if value is None or value == "" or value == [] or value == {}: + return False + + key_lower = key.lower() + + # Skip String_Columns - it's names not indices + if key == 'String_Columns': + return False + + # Skip output/path parameters + if any(x in key_lower for x in ['output', 'path', 'file', 'directory', 'save', 'export']): + return False + + # Skip regex/pattern parameters (but we'll handle Feature_Regex specially) + if 'regex' in key_lower or 'pattern' in key_lower: + return False + + # Parameters with 'column' likely have indices + if 'column' in key_lower or '_col' in key_lower: + return True + + # Known index parameters + if key in {'Annotation_s_', 'Features_to_Analyze', 'Features', 'Markers', 'Markers_to_Plot', 'Phenotypes'}: + return True + + # Check if values look like indices + if isinstance(value, list): + return all(isinstance(v, int) or (isinstance(v, str) and v.strip().isdigit()) for v in value if v) + elif isinstance(value, (int, str)): + return isinstance(value, int) or (isinstance(value, str) and value.strip().isdigit()) + + return False + +def convert_single_index(item, columns): + """Convert a single column index to name""" + if isinstance(item, str) and not item.strip().isdigit(): + return item + + try: + if isinstance(item, str): + item = int(item.strip()) + elif isinstance(item, float): + item = int(item) + except (ValueError, AttributeError): + return item + + if isinstance(item, int): + idx = item - 1 # Galaxy uses 1-based indexing + if 0 <= idx < len(columns): + return columns[idx] + elif 0 <= item < len(columns): # Fallback for 0-based + print(f"[Runner] Note: Found 0-based index {item}, converting to {columns[item]}") + return columns[item] + else: + print(f"[Runner] Warning: Index {item} out of range (have {len(columns)} columns)") + + return item + +def convert_column_indices_to_names(params, template_name): + """Convert column indices to names for templates that need it""" + + if should_skip_column_conversion(template_name): + print(f"[Runner] Skipping column conversion for {template_name}") + return params + + print(f"[Runner] Checking for column index conversion (template: {template_name})") + + # Find input file + input_file = None + input_keys = ['Upstream_Dataset', 'Upstream_Analysis', 'CSV_Files', + 'Input_File', 'Input_Dataset', 'Data_File'] + + for key in input_keys: + if key in params: + value = params[key] + if isinstance(value, list) and value: + value = value[0] + if value and os.path.exists(str(value)): + input_file = str(value) + print(f"[Runner] Found input file via {key}: {os.path.basename(input_file)}") + break + + if not input_file: + print("[Runner] No input file found for column conversion") + return params + + # Read headers + columns = read_file_headers(input_file) + if not columns: + print("[Runner] Could not read column headers, skipping conversion") + return params + + print(f"[Runner] Successfully read {len(columns)} columns") + if len(columns) <= 10: + print(f"[Runner] Columns: {columns}") + else: + print(f"[Runner] First 10 columns: {columns[:10]}") + + # Convert indices to names + converted_count = 0 + for key, value in params.items(): + # Skip non-column parameters + if not should_convert_param(key, value): + continue + + # Convert indices + if isinstance(value, list): + converted_items = [] + for item in value: + converted = convert_single_index(item, columns) + if converted is not None: + converted_items.append(converted) + converted_value = converted_items + else: + converted_value = convert_single_index(value, columns) + + if value != converted_value: + params[key] = converted_value + converted_count += 1 + print(f"[Runner] Converted {key}: {value} -> {converted_value}") + + if converted_count > 0: + print(f"[Runner] Total conversions: {converted_count} parameters") + + # CRITICAL: Handle Feature_Regex specially + if 'Feature_Regex' in params: + value = params['Feature_Regex'] + if value in [[], [""], "__ob____cb__", "[]", "", None]: + params['Feature_Regex'] = "" + print("[Runner] Cleared empty Feature_Regex parameter") + elif isinstance(value, list) and value: + params['Feature_Regex'] = "|".join(str(v) for v in value if v) + print(f"[Runner] Joined Feature_Regex list: {params['Feature_Regex']}") + + return params + +# =========================================================================== +# APPLY COLUMN CONVERSION +# =========================================================================== +print("[Runner] Step 1: Converting column indices to names") +params = convert_column_indices_to_names(params, template_name) + +# =========================================================================== +# SPECIAL HANDLING FOR SPECIFIC TEMPLATES +# =========================================================================== + +# Helper function to coerce singleton lists to strings for load_csv +def _coerce_singleton_paths_for_load_csv(params, template_name): + """For load_csv templates, flatten 1-item lists to strings for path-like params.""" + if 'load_csv' not in template_name: + return params + for key in ('CSV_Files', 'CSV_Files_Configuration'): + val = params.get(key) + if isinstance(val, list) and len(val) == 1 and isinstance(val[0], (str, bytes)): + params[key] = val[0] + print(f"[Runner] Coerced {key} from list -> string") + return params + +# Special handling for String_Columns in load_csv templates +if 'load_csv' in template_name and 'String_Columns' in params: + value = params['String_Columns'] + if not isinstance(value, list): + if value in [None, "", "[]", "__ob____cb__"]: + params['String_Columns'] = [] + elif isinstance(value, str): + s = value.strip() + if s.startswith('[') and s.endswith(']'): + try: + params['String_Columns'] = json.loads(s) + except: + params['String_Columns'] = [s] if s else [] + elif ',' in s: + params['String_Columns'] = [item.strip() for item in s.split(',') if item.strip()] + else: + params['String_Columns'] = [s] if s else [] + else: + params['String_Columns'] = [] + print(f"[Runner] Ensured String_Columns is list: {params['String_Columns']}") + +# Apply coercion for load_csv files +params = _coerce_singleton_paths_for_load_csv(params, template_name) + +# Fix for Load CSV Files directory +if 'load_csv' in template_name and 'CSV_Files' in params: + # Check if csv_input_dir was created by Galaxy command + if os.path.exists('csv_input_dir') and os.path.isdir('csv_input_dir'): + params['CSV_Files'] = 'csv_input_dir' + print("[Runner] Using csv_input_dir created by Galaxy") + elif isinstance(params['CSV_Files'], str) and os.path.isfile(params['CSV_Files']): + # We have a single file path, need to get its directory + params['CSV_Files'] = os.path.dirname(params['CSV_Files']) + print(f"[Runner] Using directory of CSV file: {params['CSV_Files']}") + +# =========================================================================== +# LIST PARAMETER NORMALIZATION +# =========================================================================== def should_normalize_as_list(key, value): """Determine if a parameter should be normalized as a list""" + # CRITICAL: Skip outputs and other non-list parameters + key_lower = key.lower() + if key_lower in {'outputs', 'output', 'upstream_analysis', 'upstream_dataset', + 'table_to_visualize', 'figure_title', 'figure_width', + 'figure_height', 'figure_dpi', 'font_size'}: + return False + + # Already a proper list? if isinstance(value, list): - return True - + # Only re-process if it's a single JSON string that needs parsing + if len(value) == 1 and isinstance(value[0], str): + s = value[0].strip() + return s.startswith('[') and s.endswith(']') + return False + + # Nothing to normalize if value is None or value == "": return False + + # CRITICAL: Explicitly mark Feature_s_to_Plot as a list parameter + if key == 'Feature_s_to_Plot' or key_lower == 'feature_s_to_plot': + return True - key_lower = key.lower() + # Other explicit list parameters + explicit_list_keys = { + 'features_to_analyze', 'features', 'markers', 'markers_to_plot', + 'phenotypes', 'labels', 'annotation_s_', 'string_columns' + } + if key_lower in explicit_list_keys: + return True + # Skip regex parameters if 'regex' in key_lower or 'pattern' in key_lower: return False + # Skip known single-value parameters if any(x in key_lower for x in ['single', 'one', 'first', 'second', 'primary']): return False - if any(x in key_lower for x in ['features', 'markers', 'phenotypes', 'annotations', - 'columns', 'types', 'labels', 'regions', 'radii']): + # Plural forms suggest lists + if any(x in key_lower for x in [ + 'features', 'markers', 'phenotypes', 'annotations', + 'columns', 'types', 'labels', 'regions', 'radii' + ]): return True + # List-like syntax in string values if isinstance(value, str): - if ',' in value or '\n' in value: - return True - if value.strip().startswith('[') and value.strip().endswith(']'): + s = value.strip() + if s.startswith('[') and s.endswith(']'): return True + # Only treat comma/newline as list separator if not in outputs-like params + if 'output' not in key_lower and 'path' not in key_lower: + if ',' in s or '\n' in s: + return True return False def normalize_to_list(value): """Convert various input formats to a proper Python list""" - if value in (None, "", "All", ["All"], "all", ["all"]): + # Handle special "All" cases first + if value in (None, "", "All", "all"): return ["All"] - + + # If it's already a list if isinstance(value, list): + # Check for already-correct lists + if value == ["All"] or value == ["all"]: + return ["All"] + + # Check if it's a single-element list with a JSON string + if len(value) == 1 and isinstance(value[0], str): + s = value[0].strip() + # If the single element looks like JSON + if s.startswith('[') and s.endswith(']'): + try: + parsed = json.loads(s) + if isinstance(parsed, list): + return parsed + except: + pass + # If single element is "All" or "all" + elif s.lower() == "all": + return ["All"] + + # Already a proper list, return as-is return value if isinstance(value, str): s = value.strip() + + # Check for "All" string + if s.lower() == "all": + return ["All"] + # Try JSON parsing if s.startswith('[') and s.endswith(']'): try: parsed = json.loads(s) @@ -257,135 +457,326 @@ def normalize_to_list(value): except: pass + # Split by comma if ',' in s: return [item.strip() for item in s.split(',') if item.strip()] + # Split by newline if '\n' in s: return [item.strip() for item in s.split('\n') if item.strip()] + # Single value return [s] if s else [] return [value] if value is not None else [] -# ============================================================================ -# MAIN EXECUTION -# ============================================================================ - -# Load parameters -with open(params_path, 'r') as f: - params = json.load(f) - print(f"[Runner] Loaded {len(params)} parameters from {params_path}") - -# Step 1: De-sanitize Galaxy parameters -print("[Runner] Step 1: De-sanitizing Galaxy parameters") -params = _maybe_parse(params) - -# Step 2: Get outputs from params (injected by Galaxy) -print("[Runner] Step 2: Getting output structure") -outputs = determine_outputs_from_params(params) - -# Create output directories -for output_type, path in outputs.items(): - if output_type != 'analysis' and path: - os.makedirs(path, exist_ok=True) - print(f"[Runner] Created {output_type} directory: {path}") - -# Step 3: Normalize list parameters -print("[Runner] Step 3: Normalizing list parameters") +# Normalize list parameters +print("[Runner] Step 2: Normalizing list parameters") list_count = 0 for key, value in list(params.items()): - if key != 'outputs' and should_normalize_as_list(key, value): + if should_normalize_as_list(key, value): original = value normalized = normalize_to_list(value) if original != normalized: params[key] = normalized list_count += 1 - print(f"[Runner] Normalized {key}: {original} -> {normalized}") + if len(str(normalized)) > 100: + print(f"[Runner] Normalized {key}: {type(original).__name__} -> list of {len(normalized)} items") + else: + print(f"[Runner] Normalized {key}: {original} -> {normalized}") if list_count > 0: print(f"[Runner] Normalized {list_count} list parameters") -# Step 4: Inject output paths -print("[Runner] Step 4: Injecting output paths") -params_exec = inject_output_paths(params, outputs, template_name) +# CRITICAL FIX: Handle single-element lists for coordinate columns +# These should be strings, not lists +coordinate_keys = ['X_Coordinate_Column', 'Y_Coordinate_Column', 'X_centroid', 'Y_centroid'] +for key in coordinate_keys: + if key in params: + value = params[key] + if isinstance(value, list) and len(value) == 1: + params[key] = value[0] + print(f"[Runner] Extracted single value from {key}: {value} -> {params[key]}") + +# Also check for any key ending with '_Column' that has a single-element list +for key in list(params.keys()): + if key.endswith('_Column') and isinstance(params[key], list) and len(params[key]) == 1: + original = params[key] + params[key] = params[key][0] + print(f"[Runner] Extracted single value from {key}: {original} -> {params[key]}") + +# =========================================================================== +# OUTPUTS HANDLING +# =========================================================================== + +# Extract outputs specification +raw_outputs = params.pop('outputs', {}) +outputs = {} + +if isinstance(raw_outputs, dict): + outputs = raw_outputs +elif isinstance(raw_outputs, str): + try: + maybe = json.loads(_unsanitize(raw_outputs)) + if isinstance(maybe, dict): + outputs = maybe + except Exception: + pass + +# CRITICAL FIX: Handle outputs if it was mistakenly normalized as a list +if isinstance(raw_outputs, list) and raw_outputs: + # Try to reconstruct the dict from the list + if len(raw_outputs) >= 2: + # Assume format like ["{'DataFrames': 'dataframe_folder'", "'figures': 'figure_folder'}"] + combined = ''.join(str(item) for item in raw_outputs) + # Clean up the string + combined = combined.replace("'", '"') + try: + outputs = json.loads(combined) + except: + # Try another approach - look for dict-like patterns + try: + dict_str = '{' + combined.split('{')[1].split('}')[0] + '}' + outputs = json.loads(dict_str.replace("'", '"')) + except: + pass -# Save parameter files -with open('params.exec.json', 'w') as f: - json.dump(params_exec, f, indent=2) +if not isinstance(outputs, dict) or not outputs: + print("[Runner] Warning: 'outputs' missing or not a dict; using defaults") + if 'boxplot' in template_name or 'plot' in template_name or 'histogram' in template_name: + outputs = {'DataFrames': 'dataframe_folder', 'figures': 'figure_folder'} + elif 'load_csv' in template_name: + outputs = {'DataFrames': 'dataframe_folder'} + elif 'interactive' in template_name: + outputs = {'html': 'html_folder'} + else: + outputs = {'analysis': 'transform_output.pickle'} + +print(f"[Runner] Outputs -> {list(outputs.keys())}") +# Create output directories +for output_type, path in outputs.items(): + if output_type != 'analysis' and path: + os.makedirs(path, exist_ok=True) + print(f"[Runner] Created {output_type} directory: {path}") + +# Add output paths to params +params['save_results'] = True + +if 'analysis' in outputs: + params['output_path'] = outputs['analysis'] + params['Output_Path'] = outputs['analysis'] + params['Output_File'] = outputs['analysis'] + +if 'DataFrames' in outputs: + df_dir = outputs['DataFrames'] + params['output_dir'] = df_dir + params['Export_Dir'] = df_dir + params['Output_File'] = os.path.join(df_dir, f'{template_name}_output.csv') + +if 'figures' in outputs: + fig_dir = outputs['figures'] + params['figure_dir'] = fig_dir + params['Figure_Dir'] = fig_dir + params['Figure_File'] = os.path.join(fig_dir, f'{template_name}.png') + +if 'html' in outputs: + html_dir = outputs['html'] + params['html_dir'] = html_dir + params['Output_File'] = os.path.join(html_dir, f'{template_name}.html') + +# Save runtime parameters +with open('params.runtime.json', 'w') as f: + json.dump(params, f, indent=2) + +# Save clean params for Galaxy display +params_display = {k: v for k, v in params.items() + if k not in ['Output_File', 'Figure_File', 'output_dir', 'figure_dir']} with open('config_used.json', 'w') as f: - params_display = {k: v for k, v in params.items() - if k != 'outputs' and - not any(x in k.lower() for x in ['output', 'save', 'path', 'dir', 'file', 'export'])} json.dump(params_display, f, indent=2) -print(f"[Runner] Saved params.exec.json ({len(params_exec)} parameters)") -print(f"[Runner] Saved config_used.json ({len(params_display)} display parameters)") +print(f"[Runner] Saved runtime parameters") -# Step 5: Load template module from installed SPAC package -print(f"[Runner] Step 5: Loading template '{template_name}' from SPAC package") +# ============================================================================ +# LOAD AND EXECUTE TEMPLATE +# ============================================================================ +# Try to import from installed package first (Docker environment) +template_module_name = template_filename.replace('.py', '') try: - # Import from spac.templates package - module_name = f'spac.templates.{template_name}_template' - mod = importlib.import_module(module_name) - print(f"[Runner] Successfully imported {module_name}") -except ImportError as e: - print(f"[Runner] ERROR: Could not import {module_name}: {e}") - print(f"[Runner] Available modules in spac.templates:") - try: - import spac.templates - import pkgutil - for importer, modname, ispkg in pkgutil.iter_modules(spac.templates.__path__): - print(f"[Runner] - {modname}") - except: - pass - sys.exit(1) + import importlib + mod = importlib.import_module(f'spac.templates.{template_module_name}') + print(f"[Runner] Loaded template from package: spac.templates.{template_module_name}") +except (ImportError, ModuleNotFoundError): + # Fallback to loading from file + print(f"[Runner] Package import failed, trying file load") + import importlib.util + + # Standard locations + template_paths = [ + f'/app/spac/templates/{template_filename}', + f'/opt/spac/templates/{template_filename}', + f'/opt/SCSAWorkflow/src/spac/templates/{template_filename}', + template_filename # Current directory + ] + + spec = None + for path in template_paths: + if os.path.exists(path): + spec = importlib.util.spec_from_file_location("template_mod", path) + if spec: + print(f"[Runner] Found template at: {path}") + break + + if not spec or not spec.loader: + print(f"[Runner] ERROR: Could not find template: {template_filename}") + sys.exit(1) + + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) +# Verify run_from_json exists if not hasattr(mod, 'run_from_json'): print('[Runner] ERROR: Template missing run_from_json function') sys.exit(2) -# Step 6: Execute template -print("[Runner] Step 6: Executing template") +# Check function signature sig = inspect.signature(mod.run_from_json) kwargs = {} if 'save_results' in sig.parameters: kwargs['save_results'] = True - print("[Runner] Added save_results=True to kwargs") - if 'show_plot' in sig.parameters: kwargs['show_plot'] = False - print("[Runner] Added show_plot=False to kwargs") +print(f"[Runner] Executing template with kwargs: {kwargs}") + +# Execute template try: - print(f"[Runner] Calling run_from_json('params.exec.json', **{kwargs})") - result = mod.run_from_json('params.exec.json', **kwargs) - print(f"[Runner] Template completed successfully, returned {type(result).__name__}") + result = mod.run_from_json('params.runtime.json', **kwargs) + print(f"[Runner] Template completed, returned: {type(result).__name__}") + + # Handle different return types + if result is not None: + if isinstance(result, dict): + print(f"[Runner] Template saved files: {list(result.keys())}") + elif isinstance(result, tuple): + # Handle tuple returns + saved_count = 0 + for i, item in enumerate(result): + if hasattr(item, 'savefig') and 'figures' in outputs: + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + fig_path = os.path.join(outputs['figures'], f'figure_{i+1}.png') + item.savefig(fig_path, dpi=300, bbox_inches='tight') + plt.close(item) + saved_count += 1 + print(f"[Runner] Saved figure to {fig_path}") + elif hasattr(item, 'to_csv') and 'DataFrames' in outputs: + df_path = os.path.join(outputs['DataFrames'], f'table_{i+1}.csv') + item.to_csv(df_path, index=True) + saved_count += 1 + print(f"[Runner] Saved DataFrame to {df_path}") + + if saved_count > 0: + print(f"[Runner] Saved {saved_count} in-memory results") + + elif hasattr(result, 'to_csv') and 'DataFrames' in outputs: + df_path = os.path.join(outputs['DataFrames'], 'output.csv') + result.to_csv(df_path, index=True) + print(f"[Runner] Saved DataFrame to {df_path}") + + elif hasattr(result, 'savefig') and 'figures' in outputs: + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + fig_path = os.path.join(outputs['figures'], 'figure.png') + result.savefig(fig_path, dpi=300, bbox_inches='tight') + plt.close(result) + print(f"[Runner] Saved figure to {fig_path}") + + elif hasattr(result, 'write_h5ad') and 'analysis' in outputs: + result.write_h5ad(outputs['analysis']) + print(f"[Runner] Saved AnnData to {outputs['analysis']}") + except Exception as e: print(f"[Runner] ERROR in template execution: {e}") print(f"[Runner] Error type: {type(e).__name__}") traceback.print_exc() + + # Debug help for common issues + if "String Columns must be a *list*" in str(e): + print("\n[Runner] DEBUG: String_Columns validation failed") + print(f"[Runner] Current String_Columns value: {params.get('String_Columns')}") + print(f"[Runner] Type: {type(params.get('String_Columns'))}") + + elif "regex pattern" in str(e).lower() or "^8$" in str(e): + print("\n[Runner] DEBUG: This appears to be a column index issue") + print("[Runner] Check that column indices were properly converted to names") + print("[Runner] Current Features_to_Analyze value:", params.get('Features_to_Analyze')) + print("[Runner] Current Feature_Regex value:", params.get('Feature_Regex')) + sys.exit(1) -# Handle any returned objects -handle_template_results(result, outputs, template_name) - -# Step 7: Verify outputs -print("[Runner] Step 7: Verifying outputs") -verify_outputs(outputs) +# Verify outputs +print("[Runner] Verifying outputs...") +found_outputs = False -print("[Runner] === Execution completed successfully ===") +for output_type, path in outputs.items(): + if output_type == 'analysis': + if os.path.exists(path): + size = os.path.getsize(path) + print(f"[Runner] ✔ {output_type}: {path} ({size:,} bytes)") + found_outputs = True + else: + print(f"[Runner] ✗ {output_type}: NOT FOUND") + else: + if os.path.exists(path) and os.path.isdir(path): + files = os.listdir(path) + if files: + print(f"[Runner] ✔ {output_type}: {len(files)} files") + for f in files[:3]: + print(f"[Runner] - {f}") + if len(files) > 3: + print(f"[Runner] ... and {len(files)-3} more") + found_outputs = True + else: + print(f"[Runner] ⚠ {output_type}: directory empty") + +# Check for files in working directory and move them +print("[Runner] Checking for files in working directory...") +for file in os.listdir('.'): + if os.path.isdir(file) or file in ['params.runtime.json', 'config_used.json', + 'tool_stdout.txt', 'outputs_returned.json']: + continue + + if file.endswith('.csv') and 'DataFrames' in outputs: + if not os.path.exists(os.path.join(outputs['DataFrames'], file)): + target = os.path.join(outputs['DataFrames'], file) + shutil.move(file, target) + print(f"[Runner] Moved {file} to {target}") + found_outputs = True + elif file.endswith(('.png', '.pdf', '.jpg', '.svg')) and 'figures' in outputs: + if not os.path.exists(os.path.join(outputs['figures'], file)): + target = os.path.join(outputs['figures'], file) + shutil.move(file, target) + print(f"[Runner] Moved {file} to {target}") + found_outputs = True + +if found_outputs: + print("[Runner] === SUCCESS ===") +else: + print("[Runner] WARNING: No outputs created") PYTHON_RUNNER EXIT_CODE=$? if [ $EXIT_CODE -ne 0 ]; then - echo "" - echo "=== Template execution failed with exit code $EXIT_CODE ===" - echo "" + echo "ERROR: Template execution failed with exit code $EXIT_CODE" + exit 1 fi -exit $EXIT_CODE \ No newline at end of file +echo "=== Execution Complete ===" +exit 0 \ No newline at end of file diff --git a/galaxy_tools/spac_boxplot/spac_boxplot.xml b/galaxy_tools/spac_boxplot/spac_boxplot.xml index 27802ed9..18d80004 100644 --- a/galaxy_tools/spac_boxplot/spac_boxplot.xml +++ b/galaxy_tools/spac_boxplot/spac_boxplot.xml @@ -1,33 +1,31 @@ - - Create a boxplot visualization of the features in the analysis dataset. - - - - nciccbr/spac:v1 - - - - - python3 - - - tool_stdout.txt && - - ## Run the universal wrapper - bash $__tool_directory__/run_spac_template.sh "$params_json" boxplot - ]]> - - - - - - - - + + Create a boxplot visualization of the features in the analysis dataset. + + + nciccbr/spac:v1 + + + + python3 + + + tool_stdout.txt && + + ## Run the universal wrapper (template name without .py extension) + bash $__tool_directory__/run_spac_template.sh "$params_json" boxplot + ]]> + + + + + + + + - + @@ -39,14 +37,12 @@ - - - - - - + + + + + + @@ -60,17 +56,37 @@ - - - - + + + + + + + @misc{spac_toolkit, + author = {FNLCR DMAP Team}, + title = {SPAC: SPAtial single-Cell analysis}, + year = {2024}, + url = {https://github.com/FNLCR-DMAP/SCSAWorkflow} + } + + + \ No newline at end of file diff --git a/galaxy_tools/spac_boxplot_update/run_spac_template.sh b/galaxy_tools/spac_boxplot_update/run_spac_template.sh deleted file mode 100644 index a93b2d6e..00000000 --- a/galaxy_tools/spac_boxplot_update/run_spac_template.sh +++ /dev/null @@ -1,710 +0,0 @@ -#!/usr/bin/env bash -# run_spac_template.sh - SPAC wrapper with column index conversion -# Version: 5.4.1 - Integrated column conversion -set -euo pipefail - -PARAMS_JSON="${1:?Missing params.json path}" -TEMPLATE_BASE="${2:?Missing template base name}" - -# Handle both base names and full .py filenames -if [[ "$TEMPLATE_BASE" == *.py ]]; then - TEMPLATE_PY="$TEMPLATE_BASE" -elif [[ "$TEMPLATE_BASE" == "load_csv_files_with_config" ]]; then - TEMPLATE_PY="load_csv_files_with_config.py" -else - TEMPLATE_PY="${TEMPLATE_BASE}_template.py" -fi - -# Use SPAC Python environment -SPAC_PYTHON="${SPAC_PYTHON:-python3}" - -echo "=== SPAC Template Wrapper v5.3 ===" -echo "Parameters: $PARAMS_JSON" -echo "Template base: $TEMPLATE_BASE" -echo "Template file: $TEMPLATE_PY" -echo "Python: $SPAC_PYTHON" - -# Run template through Python -"$SPAC_PYTHON" - <<'PYTHON_RUNNER' "$PARAMS_JSON" "$TEMPLATE_PY" 2>&1 | tee tool_stdout.txt -import json -import os -import sys -import copy -import traceback -import inspect -import shutil -import re -import csv - -# Get arguments -params_path = sys.argv[1] -template_filename = sys.argv[2] - -print(f"[Runner] Loading parameters from: {params_path}") -print(f"[Runner] Template: {template_filename}") - -# Load parameters -with open(params_path, 'r') as f: - params = json.load(f) - -# Extract template name -template_name = os.path.basename(template_filename).replace('_template.py', '').replace('.py', '') - -# =========================================================================== -# DE-SANITIZATION AND PARSING -# =========================================================================== -def _unsanitize(s: str) -> str: - """Remove Galaxy's parameter sanitization tokens""" - if not isinstance(s, str): - return s - replacements = { - '__ob__': '[', '__cb__': ']', - '__oc__': '{', '__cc__': '}', - '__dq__': '"', '__sq__': "'", - '__gt__': '>', '__lt__': '<', - '__cn__': '\n', '__cr__': '\r', - '__tc__': '\t', '__pd__': '#', - '__at__': '@', '__cm__': ',' - } - for token, char in replacements.items(): - s = s.replace(token, char) - return s - -def _maybe_parse(v): - """Recursively de-sanitize and JSON-parse strings where possible.""" - if isinstance(v, str): - u = _unsanitize(v).strip() - if (u.startswith('[') and u.endswith(']')) or (u.startswith('{') and u.endswith('}')): - try: - return json.loads(u) - except Exception: - return u - return u - elif isinstance(v, dict): - return {k: _maybe_parse(val) for k, val in v.items()} - elif isinstance(v, list): - return [_maybe_parse(item) for item in v] - return v - -# Normalize the whole params tree -params = _maybe_parse(params) - -# =========================================================================== -# COLUMN INDEX CONVERSION - CRITICAL FOR SETUP ANALYSIS -# =========================================================================== -def should_skip_column_conversion(template_name): - """Some templates don't need column index conversion""" - return 'load_csv' in template_name - -def read_file_headers(filepath): - """Read column headers from various file formats""" - try: - import pandas as pd - - # Try pandas auto-detect - try: - df = pd.read_csv(filepath, nrows=1) - if len(df.columns) > 1 or not df.columns[0].startswith('Unnamed'): - columns = df.columns.tolist() - print(f"[Runner] Pandas auto-detected delimiter, found {len(columns)} columns") - return columns - except: - pass - - # Try common delimiters - for sep in ['\t', ',', ';', '|', ' ']: - try: - df = pd.read_csv(filepath, sep=sep, nrows=1) - if len(df.columns) > 1: - columns = df.columns.tolist() - sep_name = {'\t': 'tab', ',': 'comma', ';': 'semicolon', - '|': 'pipe', ' ': 'space'}.get(sep, sep) - print(f"[Runner] Pandas found {sep_name}-delimited file with {len(columns)} columns") - return columns - except: - continue - except ImportError: - print("[Runner] pandas not available, using csv fallback") - - # CSV module fallback - try: - with open(filepath, 'r', encoding='utf-8', errors='replace', newline='') as f: - sample = f.read(8192) - f.seek(0) - - try: - dialect = csv.Sniffer().sniff(sample, delimiters='\t,;| ') - reader = csv.reader(f, dialect) - header = next(reader) - columns = [h.strip().strip('"') for h in header if h.strip()] - if columns: - print(f"[Runner] csv.Sniffer detected {len(columns)} columns") - return columns - except: - f.seek(0) - first_line = f.readline().strip() - for sep in ['\t', ',', ';', '|']: - if sep in first_line: - columns = [h.strip().strip('"') for h in first_line.split(sep)] - if len(columns) > 1: - print(f"[Runner] Manual parsing found {len(columns)} columns") - return columns - except Exception as e: - print(f"[Runner] Failed to read headers: {e}") - - return None - -def should_convert_param(key, value): - """Check if parameter contains column indices""" - if value is None or value == "" or value == [] or value == {}: - return False - - key_lower = key.lower() - - # Skip String_Columns - it's names not indices - if key == 'String_Columns': - return False - - # Skip output/path parameters - if any(x in key_lower for x in ['output', 'path', 'file', 'directory', 'save', 'export']): - return False - - # Skip regex/pattern parameters (but we'll handle Feature_Regex specially) - if 'regex' in key_lower or 'pattern' in key_lower: - return False - - # Parameters with 'column' likely have indices - if 'column' in key_lower or '_col' in key_lower: - return True - - # Known index parameters - if key in {'Annotation_s_', 'Features_to_Analyze', 'Features', 'Markers', 'Markers_to_Plot', 'Phenotypes'}: - return True - - # Check if values look like indices - if isinstance(value, list): - return all(isinstance(v, int) or (isinstance(v, str) and v.strip().isdigit()) for v in value if v) - elif isinstance(value, (int, str)): - return isinstance(value, int) or (isinstance(value, str) and value.strip().isdigit()) - - return False - -def convert_single_index(item, columns): - """Convert a single column index to name""" - if isinstance(item, str) and not item.strip().isdigit(): - return item - - try: - if isinstance(item, str): - item = int(item.strip()) - elif isinstance(item, float): - item = int(item) - except (ValueError, AttributeError): - return item - - if isinstance(item, int): - idx = item - 1 # Galaxy uses 1-based indexing - if 0 <= idx < len(columns): - return columns[idx] - elif 0 <= item < len(columns): # Fallback for 0-based - print(f"[Runner] Note: Found 0-based index {item}, converting to {columns[item]}") - return columns[item] - else: - print(f"[Runner] Warning: Index {item} out of range (have {len(columns)} columns)") - - return item - -def convert_column_indices_to_names(params, template_name): - """Convert column indices to names for templates that need it""" - - if should_skip_column_conversion(template_name): - print(f"[Runner] Skipping column conversion for {template_name}") - return params - - print(f"[Runner] Checking for column index conversion (template: {template_name})") - - # Find input file - input_file = None - input_keys = ['Upstream_Dataset', 'Upstream_Analysis', 'CSV_Files', - 'Input_File', 'Input_Dataset', 'Data_File'] - - for key in input_keys: - if key in params: - value = params[key] - if isinstance(value, list) and value: - value = value[0] - if value and os.path.exists(str(value)): - input_file = str(value) - print(f"[Runner] Found input file via {key}: {os.path.basename(input_file)}") - break - - if not input_file: - print("[Runner] No input file found for column conversion") - return params - - # Read headers - columns = read_file_headers(input_file) - if not columns: - print("[Runner] Could not read column headers, skipping conversion") - return params - - print(f"[Runner] Successfully read {len(columns)} columns") - if len(columns) <= 10: - print(f"[Runner] Columns: {columns}") - else: - print(f"[Runner] First 10 columns: {columns[:10]}") - - # Convert indices to names - converted_count = 0 - for key, value in params.items(): - # Skip non-column parameters - if not should_convert_param(key, value): - continue - - # Convert indices - if isinstance(value, list): - converted_items = [] - for item in value: - converted = convert_single_index(item, columns) - if converted is not None: - converted_items.append(converted) - converted_value = converted_items - else: - converted_value = convert_single_index(value, columns) - - if value != converted_value: - params[key] = converted_value - converted_count += 1 - print(f"[Runner] Converted {key}: {value} -> {converted_value}") - - if converted_count > 0: - print(f"[Runner] Total conversions: {converted_count} parameters") - - # CRITICAL: Handle Feature_Regex specially - if 'Feature_Regex' in params: - value = params['Feature_Regex'] - if value in [[], [""], "__ob____cb__", "[]", "", None]: - params['Feature_Regex'] = "" - print("[Runner] Cleared empty Feature_Regex parameter") - elif isinstance(value, list) and value: - params['Feature_Regex'] = "|".join(str(v) for v in value if v) - print(f"[Runner] Joined Feature_Regex list: {params['Feature_Regex']}") - - return params - -# =========================================================================== -# APPLY COLUMN CONVERSION -# =========================================================================== -print("[Runner] Step 1: Converting column indices to names") -params = convert_column_indices_to_names(params, template_name) - -# =========================================================================== -# SPECIAL HANDLING FOR SPECIFIC TEMPLATES -# =========================================================================== - -# Helper function to coerce singleton lists to strings for load_csv -def _coerce_singleton_paths_for_load_csv(params, template_name): - """For load_csv templates, flatten 1-item lists to strings for path-like params.""" - if 'load_csv' not in template_name: - return params - for key in ('CSV_Files', 'CSV_Files_Configuration'): - val = params.get(key) - if isinstance(val, list) and len(val) == 1 and isinstance(val[0], (str, bytes)): - params[key] = val[0] - print(f"[Runner] Coerced {key} from list -> string") - return params - -# Special handling for String_Columns in load_csv templates -if 'load_csv' in template_name and 'String_Columns' in params: - value = params['String_Columns'] - if not isinstance(value, list): - if value in [None, "", "[]", "__ob____cb__"]: - params['String_Columns'] = [] - elif isinstance(value, str): - s = value.strip() - if s.startswith('[') and s.endswith(']'): - try: - params['String_Columns'] = json.loads(s) - except: - params['String_Columns'] = [s] if s else [] - elif ',' in s: - params['String_Columns'] = [item.strip() for item in s.split(',') if item.strip()] - else: - params['String_Columns'] = [s] if s else [] - else: - params['String_Columns'] = [] - print(f"[Runner] Ensured String_Columns is list: {params['String_Columns']}") - -# Apply coercion for load_csv files -params = _coerce_singleton_paths_for_load_csv(params, template_name) - -# Fix for Load CSV Files directory -if 'load_csv' in template_name and 'CSV_Files' in params: - # Check if csv_input_dir was created by Galaxy command - if os.path.exists('csv_input_dir') and os.path.isdir('csv_input_dir'): - params['CSV_Files'] = 'csv_input_dir' - print("[Runner] Using csv_input_dir created by Galaxy") - elif isinstance(params['CSV_Files'], str) and os.path.isfile(params['CSV_Files']): - # We have a single file path, need to get its directory - params['CSV_Files'] = os.path.dirname(params['CSV_Files']) - print(f"[Runner] Using directory of CSV file: {params['CSV_Files']}") - -# =========================================================================== -# LIST PARAMETER NORMALIZATION -# =========================================================================== -def should_normalize_as_list(key, value): - """Determine if a parameter should be normalized as a list""" - if isinstance(value, list): - return True - - if value is None or value == "": - return False - - key_lower = key.lower() - - # Skip regex parameters - if 'regex' in key_lower or 'pattern' in key_lower: - return False - - # Skip known single-value parameters - if any(x in key_lower for x in ['single', 'one', 'first', 'second', 'primary']): - return False - - # Plural forms suggest lists - if any(x in key_lower for x in ['features', 'markers', 'phenotypes', 'annotations', - 'columns', 'types', 'labels', 'regions', 'radii']): - return True - - # Check for list separators - if isinstance(value, str): - if ',' in value or '\n' in value: - return True - if value.strip().startswith('[') and value.strip().endswith(']'): - return True - - return False - -def normalize_to_list(value): - """Convert various input formats to a proper Python list""" - if value in (None, "", "All", ["All"], "all", ["all"]): - return ["All"] - - if isinstance(value, list): - return value - - if isinstance(value, str): - s = value.strip() - - # Try JSON parsing - if s.startswith('[') and s.endswith(']'): - try: - parsed = json.loads(s) - return parsed if isinstance(parsed, list) else [str(parsed)] - except: - pass - - # Split by comma - if ',' in s: - return [item.strip() for item in s.split(',') if item.strip()] - - # Split by newline - if '\n' in s: - return [item.strip() for item in s.split('\n') if item.strip()] - - # Single value - return [s] if s else [] - - return [value] if value is not None else [] - -# Normalize list parameters -print("[Runner] Step 2: Normalizing list parameters") -list_count = 0 -for key, value in list(params.items()): - if should_normalize_as_list(key, value): - original = value - normalized = normalize_to_list(value) - if original != normalized: - params[key] = normalized - list_count += 1 - if len(str(normalized)) > 100: - print(f"[Runner] Normalized {key}: {type(original).__name__} -> list of {len(normalized)} items") - else: - print(f"[Runner] Normalized {key}: {original} -> {normalized}") - -if list_count > 0: - print(f"[Runner] Normalized {list_count} list parameters") - -# CRITICAL FIX: Handle single-element lists for coordinate columns -# These should be strings, not lists -coordinate_keys = ['X_Coordinate_Column', 'Y_Coordinate_Column', 'X_centroid', 'Y_centroid'] -for key in coordinate_keys: - if key in params: - value = params[key] - if isinstance(value, list) and len(value) == 1: - params[key] = value[0] - print(f"[Runner] Extracted single value from {key}: {value} -> {params[key]}") - -# Also check for any key ending with '_Column' that has a single-element list -for key in list(params.keys()): - if key.endswith('_Column') and isinstance(params[key], list) and len(params[key]) == 1: - original = params[key] - params[key] = params[key][0] - print(f"[Runner] Extracted single value from {key}: {original} -> {params[key]}") - -# =========================================================================== -# OUTPUTS HANDLING -# =========================================================================== - -# Extract outputs specification -raw_outputs = params.pop('outputs', {}) -outputs = {} - -if isinstance(raw_outputs, dict): - outputs = raw_outputs -elif isinstance(raw_outputs, str): - try: - maybe = json.loads(_unsanitize(raw_outputs)) - if isinstance(maybe, dict): - outputs = maybe - except Exception: - pass - -if not isinstance(outputs, dict) or not outputs: - print("[Runner] Warning: 'outputs' missing or not a dict; using defaults") - if 'boxplot' in template_name or 'plot' in template_name or 'histogram' in template_name: - outputs = {'DataFrames': 'dataframe_folder', 'figures': 'figure_folder'} - elif 'load_csv' in template_name: - outputs = {'DataFrames': 'dataframe_folder'} - elif 'interactive' in template_name: - outputs = {'html': 'html_folder'} - else: - outputs = {'analysis': 'transform_output.pickle'} - -print(f"[Runner] Outputs -> {list(outputs.keys())}") - -# Create output directories -for output_type, path in outputs.items(): - if output_type != 'analysis' and path: - os.makedirs(path, exist_ok=True) - print(f"[Runner] Created {output_type} directory: {path}") - -# Add output paths to params -params['save_results'] = True - -if 'analysis' in outputs: - params['output_path'] = outputs['analysis'] - params['Output_Path'] = outputs['analysis'] - params['Output_File'] = outputs['analysis'] - -if 'DataFrames' in outputs: - df_dir = outputs['DataFrames'] - params['output_dir'] = df_dir - params['Export_Dir'] = df_dir - params['Output_File'] = os.path.join(df_dir, f'{template_name}_output.csv') - -if 'figures' in outputs: - fig_dir = outputs['figures'] - params['figure_dir'] = fig_dir - params['Figure_Dir'] = fig_dir - params['Figure_File'] = os.path.join(fig_dir, f'{template_name}.png') - -if 'html' in outputs: - html_dir = outputs['html'] - params['html_dir'] = html_dir - params['Output_File'] = os.path.join(html_dir, f'{template_name}.html') - -# Save runtime parameters -with open('params.runtime.json', 'w') as f: - json.dump(params, f, indent=2) - -# Save clean params for Galaxy display -params_display = {k: v for k, v in params.items() - if k not in ['Output_File', 'Figure_File', 'output_dir', 'figure_dir']} -with open('config_used.json', 'w') as f: - json.dump(params_display, f, indent=2) - -print(f"[Runner] Saved runtime parameters") - -# ============================================================================ -# LOAD AND EXECUTE TEMPLATE -# ============================================================================ - -# Try to import from installed package first (Docker environment) -template_module_name = template_filename.replace('.py', '') -try: - import importlib - mod = importlib.import_module(f'spac.templates.{template_module_name}') - print(f"[Runner] Loaded template from package: spac.templates.{template_module_name}") -except (ImportError, ModuleNotFoundError): - # Fallback to loading from file - print(f"[Runner] Package import failed, trying file load") - import importlib.util - - # Standard locations - template_paths = [ - f'/app/spac/templates/{template_filename}', - f'/opt/spac/templates/{template_filename}', - f'/opt/SCSAWorkflow/src/spac/templates/{template_filename}', - template_filename # Current directory - ] - - spec = None - for path in template_paths: - if os.path.exists(path): - spec = importlib.util.spec_from_file_location("template_mod", path) - if spec: - print(f"[Runner] Found template at: {path}") - break - - if not spec or not spec.loader: - print(f"[Runner] ERROR: Could not find template: {template_filename}") - sys.exit(1) - - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - -# Verify run_from_json exists -if not hasattr(mod, 'run_from_json'): - print('[Runner] ERROR: Template missing run_from_json function') - sys.exit(2) - -# Check function signature -sig = inspect.signature(mod.run_from_json) -kwargs = {} - -if 'save_results' in sig.parameters: - kwargs['save_results'] = True -if 'show_plot' in sig.parameters: - kwargs['show_plot'] = False - -print(f"[Runner] Executing template with kwargs: {kwargs}") - -# Execute template -try: - result = mod.run_from_json('params.runtime.json', **kwargs) - print(f"[Runner] Template completed, returned: {type(result).__name__}") - - # Handle different return types - if result is not None: - if isinstance(result, dict): - print(f"[Runner] Template saved files: {list(result.keys())}") - elif isinstance(result, tuple): - # Handle tuple returns - saved_count = 0 - for i, item in enumerate(result): - if hasattr(item, 'savefig') and 'figures' in outputs: - import matplotlib - matplotlib.use('Agg') - import matplotlib.pyplot as plt - fig_path = os.path.join(outputs['figures'], f'figure_{i+1}.png') - item.savefig(fig_path, dpi=300, bbox_inches='tight') - plt.close(item) - saved_count += 1 - print(f"[Runner] Saved figure to {fig_path}") - elif hasattr(item, 'to_csv') and 'DataFrames' in outputs: - df_path = os.path.join(outputs['DataFrames'], f'table_{i+1}.csv') - item.to_csv(df_path, index=True) - saved_count += 1 - print(f"[Runner] Saved DataFrame to {df_path}") - - if saved_count > 0: - print(f"[Runner] Saved {saved_count} in-memory results") - - elif hasattr(result, 'to_csv') and 'DataFrames' in outputs: - df_path = os.path.join(outputs['DataFrames'], 'output.csv') - result.to_csv(df_path, index=True) - print(f"[Runner] Saved DataFrame to {df_path}") - - elif hasattr(result, 'savefig') and 'figures' in outputs: - import matplotlib - matplotlib.use('Agg') - import matplotlib.pyplot as plt - fig_path = os.path.join(outputs['figures'], 'figure.png') - result.savefig(fig_path, dpi=300, bbox_inches='tight') - plt.close(result) - print(f"[Runner] Saved figure to {fig_path}") - - elif hasattr(result, 'write_h5ad') and 'analysis' in outputs: - result.write_h5ad(outputs['analysis']) - print(f"[Runner] Saved AnnData to {outputs['analysis']}") - -except Exception as e: - print(f"[Runner] ERROR in template execution: {e}") - print(f"[Runner] Error type: {type(e).__name__}") - traceback.print_exc() - - # Debug help for common issues - if "String Columns must be a *list*" in str(e): - print("\n[Runner] DEBUG: String_Columns validation failed") - print(f"[Runner] Current String_Columns value: {params.get('String_Columns')}") - print(f"[Runner] Type: {type(params.get('String_Columns'))}") - - elif "regex pattern" in str(e).lower() or "^8$" in str(e): - print("\n[Runner] DEBUG: This appears to be a column index issue") - print("[Runner] Check that column indices were properly converted to names") - print("[Runner] Current Features_to_Analyze value:", params.get('Features_to_Analyze')) - print("[Runner] Current Feature_Regex value:", params.get('Feature_Regex')) - - sys.exit(1) - -# Verify outputs -print("[Runner] Verifying outputs...") -found_outputs = False - -for output_type, path in outputs.items(): - if output_type == 'analysis': - if os.path.exists(path): - size = os.path.getsize(path) - print(f"[Runner] ✔ {output_type}: {path} ({size:,} bytes)") - found_outputs = True - else: - print(f"[Runner] ✗ {output_type}: NOT FOUND") - else: - if os.path.exists(path) and os.path.isdir(path): - files = os.listdir(path) - if files: - print(f"[Runner] ✔ {output_type}: {len(files)} files") - for f in files[:3]: - print(f"[Runner] - {f}") - if len(files) > 3: - print(f"[Runner] ... and {len(files)-3} more") - found_outputs = True - else: - print(f"[Runner] ⚠ {output_type}: directory empty") - -# Check for files in working directory and move them -print("[Runner] Checking for files in working directory...") -for file in os.listdir('.'): - if os.path.isdir(file) or file in ['params.runtime.json', 'config_used.json', - 'tool_stdout.txt', 'outputs_returned.json']: - continue - - if file.endswith('.csv') and 'DataFrames' in outputs: - if not os.path.exists(os.path.join(outputs['DataFrames'], file)): - target = os.path.join(outputs['DataFrames'], file) - shutil.move(file, target) - print(f"[Runner] Moved {file} to {target}") - found_outputs = True - elif file.endswith(('.png', '.pdf', '.jpg', '.svg')) and 'figures' in outputs: - if not os.path.exists(os.path.join(outputs['figures'], file)): - target = os.path.join(outputs['figures'], file) - shutil.move(file, target) - print(f"[Runner] Moved {file} to {target}") - found_outputs = True - -if found_outputs: - print("[Runner] === SUCCESS ===") -else: - print("[Runner] WARNING: No outputs created") - -PYTHON_RUNNER - -EXIT_CODE=$? - -if [ $EXIT_CODE -ne 0 ]; then - echo "ERROR: Template execution failed with exit code $EXIT_CODE" - exit 1 -fi - -echo "=== Execution Complete ===" -exit 0 \ No newline at end of file diff --git a/galaxy_tools/spac_boxplot_update/spac_boxplot.xml b/galaxy_tools/spac_boxplot_update/spac_boxplot.xml deleted file mode 100644 index 9798c2c4..00000000 --- a/galaxy_tools/spac_boxplot_update/spac_boxplot.xml +++ /dev/null @@ -1,84 +0,0 @@ - - Create a boxplot visualization of the features in the analysis dataset. - - - nciccbr/spac:v1 - - - - python3 - - - tool_stdout.txt && - - ## Run the universal wrapper (template name without .py extension) - bash $__tool_directory__/run_spac_template.sh "$params_json" boxplot - ]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @misc{spac_toolkit, - author = {FNLCR DMAP Team}, - title = {SPAC: SPAtial single-Cell analysis}, - year = {2024}, - url = {https://github.com/FNLCR-DMAP/SCSAWorkflow} - } - - - \ No newline at end of file diff --git a/galaxy_tools/spac_interactive_spatial_plot/run_spac_template.sh b/galaxy_tools/spac_interactive_spatial_plot/run_spac_template.sh deleted file mode 100644 index f2c32068..00000000 --- a/galaxy_tools/spac_interactive_spatial_plot/run_spac_template.sh +++ /dev/null @@ -1,391 +0,0 @@ -#!/usr/bin/env bash -# run_spac_template.sh - Docker version for Galaxy -# Version: 4.0.0 - Imports templates from installed SPAC package -set -euo pipefail - -# Log everything to tool_stdout.txt -exec > >(tee -a tool_stdout.txt) 2>&1 - -PARAMS_JSON="${1:?Missing params.json path}" -TEMPLATE_NAME="${2:?Missing template name}" # Just the name, not filename - -# Use system Python inside Docker container -SPAC_PYTHON="${SPAC_PYTHON:-python3}" - -echo "=== SPAC Template Wrapper v4.0 (Docker) ===" -echo "Parameters: $PARAMS_JSON" -echo "Template: $TEMPLATE_NAME" -echo "Python: $SPAC_PYTHON" -echo "Working directory: $(pwd)" - -# Run template through Python interpreter -"$SPAC_PYTHON" - <<'PYTHON_RUNNER' "$PARAMS_JSON" "$TEMPLATE_NAME" -import json -import os -import sys -import copy -import importlib -import traceback -import inspect - -# Get command line arguments -params_path = sys.argv[1] -template_name = sys.argv[2] # Just the name like "boxplot", not "boxplot_template.py" - -print(f"[Runner] Starting execution for template: {template_name}") -print(f"[Runner] Python version: {sys.version}") - -# ============================================================================ -# HELPER FUNCTIONS -# ============================================================================ - -def determine_outputs_from_params(params): - """Read outputs from params if available, otherwise use defaults""" - if 'outputs' in params and isinstance(params['outputs'], dict): - outputs = params['outputs'] - print(f"[Runner] Using outputs from params: {list(outputs.keys())}") - return outputs - - # Fallback to defaults based on template name - print(f"[Runner] No outputs in params, using defaults for {template_name}") - - if template_name in ['boxplot', 'histogram', 'violin_plot', 'scatter_plot', - 'hierarchical_heatmap', 'relational_heatmap']: - return {'figures': 'figure_folder', 'DataFrames': 'dataframe_folder'} - elif template_name in ['interactive_spatial_plot', 'interactive_scatter_plot']: - return {'html': 'html_folder'} - elif template_name in ['analysis_to_csv', 'select_values']: - return {'DataFrames': 'dataframe_folder'} - elif template_name == 'setup_analysis': - return {'analysis': 'analysis_output.pickle'} - else: - return {'analysis': 'transform_output.pickle'} - -def inject_output_paths(params, outputs, template_name): - """Add output paths to parameters""" - params_exec = copy.deepcopy(params) - - # Remove the 'outputs' field before execution - params_exec.pop('outputs', None) - - params_exec['save_results'] = True - - if 'analysis' in outputs: - params_exec['output_path'] = outputs['analysis'] - params_exec['Output_Path'] = outputs['analysis'] - params_exec['Output_File'] = outputs['analysis'] - - if 'DataFrames' in outputs: - df_dir = outputs['DataFrames'] - params_exec['output_dir'] = df_dir - params_exec['Export_Dir'] = df_dir - params_exec['Output_File'] = os.path.join(df_dir, f'{template_name}_output.csv') - - if 'figures' in outputs: - fig_dir = outputs['figures'] - params_exec['figure_dir'] = fig_dir - params_exec['Figure_Dir'] = fig_dir - params_exec['Figure_File'] = os.path.join(fig_dir, f'{template_name}.png') - - if 'html' in outputs: - html_dir = outputs['html'] - params_exec['output_path'] = os.path.join(html_dir, f'{template_name}.html') - params_exec['Output_File'] = params_exec['output_path'] - - return params_exec - -def handle_template_results(result, outputs, template_name): - """Save any in-memory results returned by the template""" - if result is None: - return - - saved_count = 0 - - try: - import pandas as pd - import matplotlib - matplotlib.use('Agg') - import matplotlib.pyplot as plt - - if isinstance(result, tuple): - for i, item in enumerate(result): - if hasattr(item, 'savefig') and 'figures' in outputs: - fig_path = os.path.join(outputs['figures'], f'{template_name}_{i+1}.png') - item.savefig(fig_path, dpi=300, bbox_inches='tight') - print(f"[Runner] Saved figure to {fig_path}") - plt.close(item) - saved_count += 1 - elif hasattr(item, 'to_csv') and 'DataFrames' in outputs: - csv_path = os.path.join(outputs['DataFrames'], f'{template_name}_{i+1}.csv') - item.to_csv(csv_path, index=True) - print(f"[Runner] Saved DataFrame to {csv_path}") - saved_count += 1 - - elif hasattr(result, 'to_csv') and 'DataFrames' in outputs: - csv_path = os.path.join(outputs['DataFrames'], f'{template_name}_output.csv') - result.to_csv(csv_path, index=True) - print(f"[Runner] Saved DataFrame to {csv_path}") - saved_count += 1 - - elif hasattr(result, 'savefig') and 'figures' in outputs: - fig_path = os.path.join(outputs['figures'], f'{template_name}.png') - result.savefig(fig_path, dpi=300, bbox_inches='tight') - print(f"[Runner] Saved figure to {fig_path}") - plt.close(result) - saved_count += 1 - - elif hasattr(result, 'write_h5ad') and 'analysis' in outputs: - result.write_h5ad(outputs['analysis']) - print(f"[Runner] Saved AnnData to {outputs['analysis']}") - saved_count += 1 - - except ImportError as e: - print(f"[Runner] Note: Some libraries not available for result handling: {e}") - - if saved_count > 0: - print(f"[Runner] Saved {saved_count} in-memory results") - -def verify_outputs(outputs): - """Verify that expected outputs were created""" - found_outputs = False - - for output_type, path in outputs.items(): - if output_type == 'analysis': - if os.path.exists(path): - size = os.path.getsize(path) - print(f"[Runner] ✓ {output_type}: {os.path.basename(path)} ({size:,} bytes)") - found_outputs = True - else: - print(f"[Runner] ✗ {output_type}: NOT FOUND at {path}") - else: - if os.path.exists(path) and os.path.isdir(path): - files = os.listdir(path) - if files: - total_size = sum(os.path.getsize(os.path.join(path, f)) for f in files) - print(f"[Runner] ✓ {output_type}: {len(files)} files ({total_size:,} bytes)") - for f in files[:3]: - size = os.path.getsize(os.path.join(path, f)) - print(f"[Runner] - {f} ({size:,} bytes)") - if len(files) > 3: - print(f"[Runner] ... and {len(files)-3} more files") - found_outputs = True - else: - print(f"[Runner] ⚠ {output_type}: directory exists but empty") - - if not found_outputs: - print("[Runner] WARNING: No outputs were created!") - -# ============================================================================ -# PARAMETER PROCESSING -# ============================================================================ - -def _unsanitize(s: str) -> str: - """Remove Galaxy's parameter sanitization tokens""" - replacements = { - '__ob__': '[', '__cb__': ']', - '__oc__': '{', '__cc__': '}', - '__dq__': '"', '__sq__': "'", - '__gt__': '>', '__lt__': '<', - '__cn__': '\n', '__cr__': '\r', - '__tc__': '\t', '__pd__': '#', - '__at__': '@', '__cm__': ',', - '__dollar__': '$', '__us__': '_' - } - for token, char in replacements.items(): - s = s.replace(token, char) - return s - -def _maybe_parse(v): - """Recursively unsanitize and parse JSON where possible""" - if isinstance(v, str): - u = _unsanitize(v).strip() - if (u.startswith('[') and u.endswith(']')) or (u.startswith('{') and u.endswith('}')): - try: - return json.loads(u) - except: - return u - return u - elif isinstance(v, dict): - return {k: _maybe_parse(val) for k, val in v.items()} - elif isinstance(v, list): - return [_maybe_parse(item) for item in v] - return v - -def should_normalize_as_list(key, value): - """Determine if a parameter should be normalized as a list""" - if isinstance(value, list): - return True - - if value is None or value == "": - return False - - key_lower = key.lower() - - if 'regex' in key_lower or 'pattern' in key_lower: - return False - - if any(x in key_lower for x in ['single', 'one', 'first', 'second', 'primary']): - return False - - if any(x in key_lower for x in ['features', 'markers', 'phenotypes', 'annotations', - 'columns', 'types', 'labels', 'regions', 'radii']): - return True - - if isinstance(value, str): - if ',' in value or '\n' in value: - return True - if value.strip().startswith('[') and value.strip().endswith(']'): - return True - - return False - -def normalize_to_list(value): - """Convert various input formats to a proper Python list""" - if value in (None, "", "All", ["All"], "all", ["all"]): - return ["All"] - - if isinstance(value, list): - return value - - if isinstance(value, str): - s = value.strip() - - if s.startswith('[') and s.endswith(']'): - try: - parsed = json.loads(s) - return parsed if isinstance(parsed, list) else [str(parsed)] - except: - pass - - if ',' in s: - return [item.strip() for item in s.split(',') if item.strip()] - - if '\n' in s: - return [item.strip() for item in s.split('\n') if item.strip()] - - return [s] if s else [] - - return [value] if value is not None else [] - -# ============================================================================ -# MAIN EXECUTION -# ============================================================================ - -# Load parameters -with open(params_path, 'r') as f: - params = json.load(f) - print(f"[Runner] Loaded {len(params)} parameters from {params_path}") - -# Step 1: De-sanitize Galaxy parameters -print("[Runner] Step 1: De-sanitizing Galaxy parameters") -params = _maybe_parse(params) - -# Step 2: Get outputs from params (injected by Galaxy) -print("[Runner] Step 2: Getting output structure") -outputs = determine_outputs_from_params(params) - -# Create output directories -for output_type, path in outputs.items(): - if output_type != 'analysis' and path: - os.makedirs(path, exist_ok=True) - print(f"[Runner] Created {output_type} directory: {path}") - -# Step 3: Normalize list parameters -print("[Runner] Step 3: Normalizing list parameters") -list_count = 0 -for key, value in list(params.items()): - if key != 'outputs' and should_normalize_as_list(key, value): - original = value - normalized = normalize_to_list(value) - if original != normalized: - params[key] = normalized - list_count += 1 - print(f"[Runner] Normalized {key}: {original} -> {normalized}") - -if list_count > 0: - print(f"[Runner] Normalized {list_count} list parameters") - -# Step 4: Inject output paths -print("[Runner] Step 4: Injecting output paths") -params_exec = inject_output_paths(params, outputs, template_name) - -# Save parameter files -with open('params.exec.json', 'w') as f: - json.dump(params_exec, f, indent=2) - -with open('config_used.json', 'w') as f: - params_display = {k: v for k, v in params.items() - if k != 'outputs' and - not any(x in k.lower() for x in ['output', 'save', 'path', 'dir', 'file', 'export'])} - json.dump(params_display, f, indent=2) - -print(f"[Runner] Saved params.exec.json ({len(params_exec)} parameters)") -print(f"[Runner] Saved config_used.json ({len(params_display)} display parameters)") - -# Step 5: Load template module from installed SPAC package -print(f"[Runner] Step 5: Loading template '{template_name}' from SPAC package") - -try: - # Import from spac.templates package - module_name = f'spac.templates.{template_name}_template' - mod = importlib.import_module(module_name) - print(f"[Runner] Successfully imported {module_name}") -except ImportError as e: - print(f"[Runner] ERROR: Could not import {module_name}: {e}") - print(f"[Runner] Available modules in spac.templates:") - try: - import spac.templates - import pkgutil - for importer, modname, ispkg in pkgutil.iter_modules(spac.templates.__path__): - print(f"[Runner] - {modname}") - except: - pass - sys.exit(1) - -if not hasattr(mod, 'run_from_json'): - print('[Runner] ERROR: Template missing run_from_json function') - sys.exit(2) - -# Step 6: Execute template -print("[Runner] Step 6: Executing template") -sig = inspect.signature(mod.run_from_json) -kwargs = {} - -if 'save_results' in sig.parameters: - kwargs['save_results'] = True - print("[Runner] Added save_results=True to kwargs") - -if 'show_plot' in sig.parameters: - kwargs['show_plot'] = False - print("[Runner] Added show_plot=False to kwargs") - -try: - print(f"[Runner] Calling run_from_json('params.exec.json', **{kwargs})") - result = mod.run_from_json('params.exec.json', **kwargs) - print(f"[Runner] Template completed successfully, returned {type(result).__name__}") -except Exception as e: - print(f"[Runner] ERROR in template execution: {e}") - print(f"[Runner] Error type: {type(e).__name__}") - traceback.print_exc() - sys.exit(1) - -# Handle any returned objects -handle_template_results(result, outputs, template_name) - -# Step 7: Verify outputs -print("[Runner] Step 7: Verifying outputs") -verify_outputs(outputs) - -print("[Runner] === Execution completed successfully ===") - -PYTHON_RUNNER - -EXIT_CODE=$? - -if [ $EXIT_CODE -ne 0 ]; then - echo "" - echo "=== Template execution failed with exit code $EXIT_CODE ===" - echo "" -fi - -exit $EXIT_CODE \ No newline at end of file diff --git a/galaxy_tools/spac_interactive_spatial_plot/spac_interactive_spatial_plot.xml b/galaxy_tools/spac_interactive_spatial_plot/spac_interactive_spatial_plot.xml deleted file mode 100644 index 68b49576..00000000 --- a/galaxy_tools/spac_interactive_spatial_plot/spac_interactive_spatial_plot.xml +++ /dev/null @@ -1,92 +0,0 @@ - - Creates interactive, real-time visual explorations of single-cell spatial data for in-depth analysis. - - - nciccbr/spac:v1 - - - - python3 - - - tool_stdout.txt && - - ## Run the universal wrapper - bash $__tool_directory__/run_spac_template.sh "$params_json" interactive_spatial_plot - ]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -@misc{spac_toolkit, - author = {FNLCR DMAP Team}, - title = {SPAC: SPAtial single-Cell analysis}, - year = {2023}, - url = {https://github.com/FNLCR-DMAP/SCSAWorkflow} -} - - - \ No newline at end of file diff --git a/galaxy_tools/spac_load_csv_files/run_spac_template.sh b/galaxy_tools/spac_load_csv_files/run_spac_template.sh index a67c68eb..4ec7c784 100644 --- a/galaxy_tools/spac_load_csv_files/run_spac_template.sh +++ b/galaxy_tools/spac_load_csv_files/run_spac_template.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash -# run_spac_template.sh - SPAC wrapper with Load CSV fixes -# Version: 5.3.0 - Complete version with Load CSV handling +# run_spac_template.sh - SPAC wrapper with column index conversion +# Version: 5.5.0 - Fixed load_csv_files to output single CSV set -euo pipefail PARAMS_JSON="${1:?Missing params.json path}" TEMPLATE_BASE="${2:?Missing template base name}" -# Handle both base names and full .py filenames for backward compatibility +# Handle both base names and full .py filenames if [[ "$TEMPLATE_BASE" == *.py ]]; then TEMPLATE_PY="$TEMPLATE_BASE" elif [[ "$TEMPLATE_BASE" == "load_csv_files_with_config" ]]; then @@ -18,7 +18,7 @@ fi # Use SPAC Python environment SPAC_PYTHON="${SPAC_PYTHON:-python3}" -echo "=== SPAC Template Wrapper v5.3 ===" +echo "=== SPAC Template Wrapper v5.5 ===" echo "Parameters: $PARAMS_JSON" echo "Template base: $TEMPLATE_BASE" echo "Template file: $TEMPLATE_PY" @@ -33,6 +33,8 @@ import copy import traceback import inspect import shutil +import re +import csv # Get arguments params_path = sys.argv[1] @@ -45,9 +47,12 @@ print(f"[Runner] Template: {template_filename}") with open(params_path, 'r') as f: params = json.load(f) -# --------------------------------------------------------------------------- -# De-sanitization and parsing helpers -# --------------------------------------------------------------------------- +# Extract template name +template_name = os.path.basename(template_filename).replace('_template.py', '').replace('.py', '') + +# =========================================================================== +# DE-SANITIZATION AND PARSING +# =========================================================================== def _unsanitize(s: str) -> str: """Remove Galaxy's parameter sanitization tokens""" if not isinstance(s, str): @@ -81,11 +86,221 @@ def _maybe_parse(v): return [_maybe_parse(item) for item in v] return v -# First normalize the whole params tree +# Normalize the whole params tree params = _maybe_parse(params) -# Extract template name -template_name = os.path.basename(template_filename).replace('_template.py', '').replace('.py', '') +# =========================================================================== +# COLUMN INDEX CONVERSION - CRITICAL FOR SETUP ANALYSIS +# =========================================================================== +def should_skip_column_conversion(template_name): + """Some templates don't need column index conversion""" + return 'load_csv' in template_name + +def read_file_headers(filepath): + """Read column headers from various file formats""" + try: + import pandas as pd + + # Try pandas auto-detect + try: + df = pd.read_csv(filepath, nrows=1) + if len(df.columns) > 1 or not df.columns[0].startswith('Unnamed'): + columns = df.columns.tolist() + print(f"[Runner] Pandas auto-detected delimiter, found {len(columns)} columns") + return columns + except: + pass + + # Try common delimiters + for sep in ['\t', ',', ';', '|', ' ']: + try: + df = pd.read_csv(filepath, sep=sep, nrows=1) + if len(df.columns) > 1: + columns = df.columns.tolist() + sep_name = {'\t': 'tab', ',': 'comma', ';': 'semicolon', + '|': 'pipe', ' ': 'space'}.get(sep, sep) + print(f"[Runner] Pandas found {sep_name}-delimited file with {len(columns)} columns") + return columns + except: + continue + except ImportError: + print("[Runner] pandas not available, using csv fallback") + + # CSV module fallback + try: + with open(filepath, 'r', encoding='utf-8', errors='replace', newline='') as f: + sample = f.read(8192) + f.seek(0) + + try: + dialect = csv.Sniffer().sniff(sample, delimiters='\t,;| ') + reader = csv.reader(f, dialect) + header = next(reader) + columns = [h.strip().strip('"') for h in header if h.strip()] + if columns: + print(f"[Runner] csv.Sniffer detected {len(columns)} columns") + return columns + except: + f.seek(0) + first_line = f.readline().strip() + for sep in ['\t', ',', ';', '|']: + if sep in first_line: + columns = [h.strip().strip('"') for h in first_line.split(sep)] + if len(columns) > 1: + print(f"[Runner] Manual parsing found {len(columns)} columns") + return columns + except Exception as e: + print(f"[Runner] Failed to read headers: {e}") + + return None + +def should_convert_param(key, value): + """Check if parameter contains column indices""" + if value is None or value == "" or value == [] or value == {}: + return False + + key_lower = key.lower() + + # Skip String_Columns - it's names not indices + if key == 'String_Columns': + return False + + # Skip output/path parameters + if any(x in key_lower for x in ['output', 'path', 'file', 'directory', 'save', 'export']): + return False + + # Skip regex/pattern parameters (but we'll handle Feature_Regex specially) + if 'regex' in key_lower or 'pattern' in key_lower: + return False + + # Parameters with 'column' likely have indices + if 'column' in key_lower or '_col' in key_lower: + return True + + # Known index parameters + if key in {'Annotation_s_', 'Features_to_Analyze', 'Features', 'Markers', 'Markers_to_Plot', 'Phenotypes'}: + return True + + # Check if values look like indices + if isinstance(value, list): + return all(isinstance(v, int) or (isinstance(v, str) and v.strip().isdigit()) for v in value if v) + elif isinstance(value, (int, str)): + return isinstance(value, int) or (isinstance(value, str) and value.strip().isdigit()) + + return False + +def convert_single_index(item, columns): + """Convert a single column index to name""" + if isinstance(item, str) and not item.strip().isdigit(): + return item + + try: + if isinstance(item, str): + item = int(item.strip()) + elif isinstance(item, float): + item = int(item) + except (ValueError, AttributeError): + return item + + if isinstance(item, int): + idx = item - 1 # Galaxy uses 1-based indexing + if 0 <= idx < len(columns): + return columns[idx] + elif 0 <= item < len(columns): # Fallback for 0-based + print(f"[Runner] Note: Found 0-based index {item}, converting to {columns[item]}") + return columns[item] + else: + print(f"[Runner] Warning: Index {item} out of range (have {len(columns)} columns)") + + return item + +def convert_column_indices_to_names(params, template_name): + """Convert column indices to names for templates that need it""" + + if should_skip_column_conversion(template_name): + print(f"[Runner] Skipping column conversion for {template_name}") + return params + + print(f"[Runner] Checking for column index conversion (template: {template_name})") + + # Find input file + input_file = None + input_keys = ['Upstream_Dataset', 'Upstream_Analysis', 'CSV_Files', + 'Input_File', 'Input_Dataset', 'Data_File'] + + for key in input_keys: + if key in params: + value = params[key] + if isinstance(value, list) and value: + value = value[0] + if value and os.path.exists(str(value)): + input_file = str(value) + print(f"[Runner] Found input file via {key}: {os.path.basename(input_file)}") + break + + if not input_file: + print("[Runner] No input file found for column conversion") + return params + + # Read headers + columns = read_file_headers(input_file) + if not columns: + print("[Runner] Could not read column headers, skipping conversion") + return params + + print(f"[Runner] Successfully read {len(columns)} columns") + if len(columns) <= 10: + print(f"[Runner] Columns: {columns}") + else: + print(f"[Runner] First 10 columns: {columns[:10]}") + + # Convert indices to names + converted_count = 0 + for key, value in params.items(): + # Skip non-column parameters + if not should_convert_param(key, value): + continue + + # Convert indices + if isinstance(value, list): + converted_items = [] + for item in value: + converted = convert_single_index(item, columns) + if converted is not None: + converted_items.append(converted) + converted_value = converted_items + else: + converted_value = convert_single_index(value, columns) + + if value != converted_value: + params[key] = converted_value + converted_count += 1 + print(f"[Runner] Converted {key}: {value} -> {converted_value}") + + if converted_count > 0: + print(f"[Runner] Total conversions: {converted_count} parameters") + + # CRITICAL: Handle Feature_Regex specially + if 'Feature_Regex' in params: + value = params['Feature_Regex'] + if value in [[], [""], "__ob____cb__", "[]", "", None]: + params['Feature_Regex'] = "" + print("[Runner] Cleared empty Feature_Regex parameter") + elif isinstance(value, list) and value: + params['Feature_Regex'] = "|".join(str(v) for v in value if v) + print(f"[Runner] Joined Feature_Regex list: {params['Feature_Regex']}") + + return params + +# =========================================================================== +# APPLY COLUMN CONVERSION +# =========================================================================== +print("[Runner] Step 1: Converting column indices to names") +params = convert_column_indices_to_names(params, template_name) + +# =========================================================================== +# SPECIAL HANDLING FOR SPECIFIC TEMPLATES +# =========================================================================== # Helper function to coerce singleton lists to strings for load_csv def _coerce_singleton_paths_for_load_csv(params, template_name): @@ -120,10 +335,10 @@ if 'load_csv' in template_name and 'String_Columns' in params: params['String_Columns'] = [] print(f"[Runner] Ensured String_Columns is list: {params['String_Columns']}") -# Apply the coercion for load_csv files +# Apply coercion for load_csv files params = _coerce_singleton_paths_for_load_csv(params, template_name) -# CRITICAL FIX: For Load CSV Files, check if we have csv_input_dir +# Fix for Load CSV Files directory if 'load_csv' in template_name and 'CSV_Files' in params: # Check if csv_input_dir was created by Galaxy command if os.path.exists('csv_input_dir') and os.path.isdir('csv_input_dir'): @@ -134,6 +349,112 @@ if 'load_csv' in template_name and 'CSV_Files' in params: params['CSV_Files'] = os.path.dirname(params['CSV_Files']) print(f"[Runner] Using directory of CSV file: {params['CSV_Files']}") +# =========================================================================== +# LIST PARAMETER NORMALIZATION +# =========================================================================== +def should_normalize_as_list(key, value): + """Determine if a parameter should be normalized as a list""" + if isinstance(value, list): + return True + + if value is None or value == "": + return False + + key_lower = key.lower() + + # Skip regex parameters + if 'regex' in key_lower or 'pattern' in key_lower: + return False + + # Skip known single-value parameters + if any(x in key_lower for x in ['single', 'one', 'first', 'second', 'primary']): + return False + + # Plural forms suggest lists + if any(x in key_lower for x in ['features', 'markers', 'phenotypes', 'annotations', + 'columns', 'types', 'labels', 'regions', 'radii']): + return True + + # Check for list separators + if isinstance(value, str): + if ',' in value or '\n' in value: + return True + if value.strip().startswith('[') and value.strip().endswith(']'): + return True + + return False + +def normalize_to_list(value): + """Convert various input formats to a proper Python list""" + if value in (None, "", "All", ["All"], "all", ["all"]): + return ["All"] + + if isinstance(value, list): + return value + + if isinstance(value, str): + s = value.strip() + + # Try JSON parsing + if s.startswith('[') and s.endswith(']'): + try: + parsed = json.loads(s) + return parsed if isinstance(parsed, list) else [str(parsed)] + except: + pass + + # Split by comma + if ',' in s: + return [item.strip() for item in s.split(',') if item.strip()] + + # Split by newline + if '\n' in s: + return [item.strip() for item in s.split('\n') if item.strip()] + + # Single value + return [s] if s else [] + + return [value] if value is not None else [] + +# Normalize list parameters +print("[Runner] Step 2: Normalizing list parameters") +list_count = 0 +for key, value in list(params.items()): + if should_normalize_as_list(key, value): + original = value + normalized = normalize_to_list(value) + if original != normalized: + params[key] = normalized + list_count += 1 + if len(str(normalized)) > 100: + print(f"[Runner] Normalized {key}: {type(original).__name__} -> list of {len(normalized)} items") + else: + print(f"[Runner] Normalized {key}: {original} -> {normalized}") + +if list_count > 0: + print(f"[Runner] Normalized {list_count} list parameters") + +# CRITICAL FIX: Handle single-element lists for coordinate columns +# These should be strings, not lists +coordinate_keys = ['X_Coordinate_Column', 'Y_Coordinate_Column', 'X_centroid', 'Y_centroid'] +for key in coordinate_keys: + if key in params: + value = params[key] + if isinstance(value, list) and len(value) == 1: + params[key] = value[0] + print(f"[Runner] Extracted single value from {key}: {value} -> {params[key]}") + +# Also check for any key ending with '_Column' that has a single-element list +for key in list(params.keys()): + if key.endswith('_Column') and isinstance(params[key], list) and len(params[key]) == 1: + original = params[key] + params[key] = params[key][0] + print(f"[Runner] Extracted single value from {key}: {original} -> {params[key]}") + +# =========================================================================== +# OUTPUTS HANDLING +# =========================================================================== + # Extract outputs specification raw_outputs = params.pop('outputs', {}) outputs = {} @@ -167,34 +488,6 @@ for output_type, path in outputs.items(): os.makedirs(path, exist_ok=True) print(f"[Runner] Created {output_type} directory: {path}") -# Normalize list parameters for features -feature_keys = [ - 'Feature_s_to_Plot', 'Features_to_Plot', 'features', - 'Features', 'Phenotypes', 'Markers', 'Regions' -] - -for key in feature_keys: - if key in params: - value = params[key] - if value in (None, "", "All", ["All"], "all", ["all"]): - params[key] = ["All"] - elif isinstance(value, str): - u = _unsanitize(value).strip() - if u.startswith('[') and u.endswith(']'): - try: - params[key] = json.loads(u) - except: - if ',' in u: - params[key] = [s.strip() for s in u.split(',') if s.strip()] - else: - params[key] = [u] if u else [] - elif ',' in u: - params[key] = [s.strip() for s in u.split(',') if s.strip()] - elif '\n' in u: - params[key] = [s.strip() for s in u.split('\n') if s.strip()] - else: - params[key] = [u] if u else [] - # Add output paths to params params['save_results'] = True @@ -207,7 +500,11 @@ if 'DataFrames' in outputs: df_dir = outputs['DataFrames'] params['output_dir'] = df_dir params['Export_Dir'] = df_dir - params['Output_File'] = os.path.join(df_dir, f'{template_name}_output.csv') + # For load_csv, use a specific filename for the combined dataframe + if 'load_csv' in template_name: + params['Output_File'] = os.path.join(df_dir, 'combined_dataframe.csv') + else: + params['Output_File'] = os.path.join(df_dir, f'{template_name}_output.csv') if 'figures' in outputs: fig_dir = outputs['figures'] @@ -233,7 +530,7 @@ with open('config_used.json', 'w') as f: print(f"[Runner] Saved runtime parameters") # ============================================================================ -# LOAD AND EXECUTE TEMPLATE - CRITICAL: THIS MUST BE COMPLETE +# LOAD AND EXECUTE TEMPLATE # ============================================================================ # Try to import from installed package first (Docker environment) @@ -247,7 +544,7 @@ except (ImportError, ModuleNotFoundError): print(f"[Runner] Package import failed, trying file load") import importlib.util - # Try standard locations + # Standard locations template_paths = [ f'/app/spac/templates/{template_filename}', f'/opt/spac/templates/{template_filename}', @@ -291,8 +588,80 @@ try: result = mod.run_from_json('params.runtime.json', **kwargs) print(f"[Runner] Template completed, returned: {type(result).__name__}") - # Handle different return types - if result is not None: + # =========================================================================== + # SPECIAL HANDLING FOR LOAD_CSV_FILES TEMPLATE + # =========================================================================== + if 'load_csv' in template_name: + print("[Runner] Special handling for load_csv_files template") + + # The template should return a DataFrame or save CSV files + if result is not None: + try: + import pandas as pd + + # If result is a DataFrame, save it directly + if hasattr(result, 'to_csv'): + output_path = os.path.join(outputs.get('DataFrames', 'dataframe_folder'), 'combined_dataframe.csv') + result.to_csv(output_path, index=False, header=True) + print(f"[Runner] Saved combined DataFrame to {output_path}") + + # If result is a dict of DataFrames, combine them + elif isinstance(result, dict): + dfs = [] + for name, df in result.items(): + if hasattr(df, 'to_csv'): + # Add a source column to track origin + df['_source_file'] = name + dfs.append(df) + + if dfs: + combined = pd.concat(dfs, ignore_index=True) + output_path = os.path.join(outputs.get('DataFrames', 'dataframe_folder'), 'combined_dataframe.csv') + combined.to_csv(output_path, index=False, header=True) + print(f"[Runner] Combined {len(dfs)} DataFrames into {output_path}") + except Exception as e: + print(f"[Runner] Could not combine DataFrames: {e}") + + # Check if CSV files were saved in the dataframe folder + df_dir = outputs.get('DataFrames', 'dataframe_folder') + if os.path.exists(df_dir): + csv_files = [f for f in os.listdir(df_dir) if f.endswith('.csv')] + + # If we have multiple CSV files but no combined_dataframe.csv, create it + if len(csv_files) > 1 and 'combined_dataframe.csv' not in csv_files: + try: + import pandas as pd + dfs = [] + for csv_file in csv_files: + filepath = os.path.join(df_dir, csv_file) + df = pd.read_csv(filepath) + df['_source_file'] = csv_file.replace('.csv', '') + dfs.append(df) + + combined = pd.concat(dfs, ignore_index=True) + output_path = os.path.join(df_dir, 'combined_dataframe.csv') + combined.to_csv(output_path, index=False, header=True) + print(f"[Runner] Combined {len(csv_files)} CSV files into {output_path}") + except Exception as e: + print(f"[Runner] Could not combine CSV files: {e}") + # If combination fails, just rename the first CSV + if csv_files: + src = os.path.join(df_dir, csv_files[0]) + dst = os.path.join(df_dir, 'combined_dataframe.csv') + shutil.copy2(src, dst) + print(f"[Runner] Copied {csv_files[0]} to combined_dataframe.csv") + + # If we have exactly one CSV file and it's not named combined_dataframe.csv, rename it + elif len(csv_files) == 1 and csv_files[0] != 'combined_dataframe.csv': + src = os.path.join(df_dir, csv_files[0]) + dst = os.path.join(df_dir, 'combined_dataframe.csv') + shutil.move(src, dst) + print(f"[Runner] Renamed {csv_files[0]} to combined_dataframe.csv") + + # =========================================================================== + # HANDLE OTHER RETURN TYPES + # =========================================================================== + elif result is not None: if isinstance(result, dict): print(f"[Runner] Template saved files: {list(result.keys())}") elif isinstance(result, tuple): @@ -319,7 +688,7 @@ try: elif hasattr(result, 'to_csv') and 'DataFrames' in outputs: df_path = os.path.join(outputs['DataFrames'], 'output.csv') - result.to_csv(df_path, index=True) + result.to_csv(df_path, index=False, header=True) print(f"[Runner] Saved DataFrame to {df_path}") elif hasattr(result, 'savefig') and 'figures' in outputs: @@ -337,7 +706,21 @@ try: except Exception as e: print(f"[Runner] ERROR in template execution: {e}") + print(f"[Runner] Error type: {type(e).__name__}") traceback.print_exc() + + # Debug help for common issues + if "String Columns must be a *list*" in str(e): + print("\n[Runner] DEBUG: String_Columns validation failed") + print(f"[Runner] Current String_Columns value: {params.get('String_Columns')}") + print(f"[Runner] Type: {type(params.get('String_Columns'))}") + + elif "regex pattern" in str(e).lower() or "^8$" in str(e): + print("\n[Runner] DEBUG: This appears to be a column index issue") + print("[Runner] Check that column indices were properly converted to names") + print("[Runner] Current Features_to_Analyze value:", params.get('Features_to_Analyze')) + print("[Runner] Current Feature_Regex value:", params.get('Feature_Regex')) + sys.exit(1) # Verify outputs @@ -348,7 +731,7 @@ for output_type, path in outputs.items(): if output_type == 'analysis': if os.path.exists(path): size = os.path.getsize(path) - print(f"[Runner] ✔ {output_type}: {path} ({size:,} bytes)") + print(f"[Runner] ✓ {output_type}: {path} ({size:,} bytes)") found_outputs = True else: print(f"[Runner] ✗ {output_type}: NOT FOUND") @@ -356,7 +739,7 @@ for output_type, path in outputs.items(): if os.path.exists(path) and os.path.isdir(path): files = os.listdir(path) if files: - print(f"[Runner] ✔ {output_type}: {len(files)} files") + print(f"[Runner] ✓ {output_type}: {len(files)} files") for f in files[:3]: print(f"[Runner] - {f}") if len(files) > 3: diff --git a/galaxy_tools/spac_load_csv_files/spac_load_csv_files.xml b/galaxy_tools/spac_load_csv_files/spac_load_csv_files.xml index 44901319..ec185659 100644 --- a/galaxy_tools/spac_load_csv_files/spac_load_csv_files.xml +++ b/galaxy_tools/spac_load_csv_files/spac_load_csv_files.xml @@ -48,14 +48,19 @@ - - - - - - - - + + + + {items}") + elif not isinstance(value, list): + params['Annotation_s_'] = [] + else: + params['Annotation_s_'] = [] + + # Handle Feature_s_ (text area, can be comma-separated or newline-separated) + if 'Feature_s_' in params: + value = params['Feature_s_'] + if value: + # Convert to list if it's a string + if isinstance(value, str): + # Check for comma separation first, then newline + if ',' in value: + items = [item.strip() for item in value.split(',') if item.strip()] + elif '\n' in value: + items = [item.strip() for item in value.split('\n') if item.strip()] + else: + # Single value + items = [value.strip()] if value.strip() else [] + params['Feature_s_'] = items + print(f"[Runner] Parsed Feature_s_: {len(items)} items") + if len(items) <= 10: + print(f"[Runner] Features: {items}") + elif not isinstance(value, list): + params['Feature_s_'] = [] + else: + params['Feature_s_'] = [] + + # Handle Feature_Regex (optional text field) + if 'Feature_Regex' in params: + value = params['Feature_Regex'] + if value in [[], [""], "__ob____cb__", "[]", "", None]: + params['Feature_Regex'] = "" + elif isinstance(value, str): + params['Feature_Regex'] = value.strip() + print(f"[Runner] Feature_Regex = '{params.get('Feature_Regex', '')}'") + + return params + +# =========================================================================== +# COLUMN INDEX CONVERSION - For tools using column indices # =========================================================================== def should_skip_column_conversion(template_name): """Some templates don't need column index conversion""" - return 'load_csv' in template_name + # setup_analysis uses text inputs now, not indices + return 'load_csv' in template_name or 'setup_analysis' in template_name def read_file_headers(filepath): """Read column headers from various file formats""" @@ -165,11 +242,15 @@ def should_convert_param(key, value): if key == 'String_Columns': return False + # Skip the text-based parameters from setup_analysis + if key in ['X_centroid', 'Y_centroid', 'Annotation_s_', 'Feature_s_', 'Feature_Regex']: + return False + # Skip output/path parameters if any(x in key_lower for x in ['output', 'path', 'file', 'directory', 'save', 'export']): return False - # Skip regex/pattern parameters (but we'll handle Feature_Regex specially) + # Skip regex/pattern parameters if 'regex' in key_lower or 'pattern' in key_lower: return False @@ -177,8 +258,8 @@ def should_convert_param(key, value): if 'column' in key_lower or '_col' in key_lower: return True - # Known index parameters - if key in {'Annotation_s_', 'Features_to_Analyze', 'Features', 'Markers', 'Markers_to_Plot', 'Phenotypes'}: + # Known index parameters (but not the text-based ones) + if key in {'Features_to_Analyze', 'Features', 'Markers', 'Markers_to_Plot', 'Phenotypes'}: return True # Check if values look like indices @@ -280,22 +361,15 @@ def convert_column_indices_to_names(params, template_name): if converted_count > 0: print(f"[Runner] Total conversions: {converted_count} parameters") - # CRITICAL: Handle Feature_Regex specially - if 'Feature_Regex' in params: - value = params['Feature_Regex'] - if value in [[], [""], "__ob____cb__", "[]", "", None]: - params['Feature_Regex'] = "" - print("[Runner] Cleared empty Feature_Regex parameter") - elif isinstance(value, list) and value: - params['Feature_Regex'] = "|".join(str(v) for v in value if v) - print(f"[Runner] Joined Feature_Regex list: {params['Feature_Regex']}") - return params # =========================================================================== -# APPLY COLUMN CONVERSION +# APPLY TEXT PROCESSING AND COLUMN CONVERSION # =========================================================================== -print("[Runner] Step 1: Converting column indices to names") +print("[Runner] Step 1: Processing text inputs for setup_analysis") +params = process_setup_analysis_text_inputs(params, template_name) + +print("[Runner] Step 2: Converting column indices to names (if needed)") params = convert_column_indices_to_names(params, template_name) # =========================================================================== @@ -329,6 +403,8 @@ if 'load_csv' in template_name and 'String_Columns' in params: params['String_Columns'] = [s] if s else [] elif ',' in s: params['String_Columns'] = [item.strip() for item in s.split(',') if item.strip()] + elif '\n' in s: + params['String_Columns'] = [item.strip() for item in s.split('\n') if item.strip()] else: params['String_Columns'] = [s] if s else [] else: @@ -350,10 +426,14 @@ if 'load_csv' in template_name and 'CSV_Files' in params: print(f"[Runner] Using directory of CSV file: {params['CSV_Files']}") # =========================================================================== -# LIST PARAMETER NORMALIZATION +# LIST PARAMETER NORMALIZATION (for other tools) # =========================================================================== def should_normalize_as_list(key, value): """Determine if a parameter should be normalized as a list""" + # Skip if already handled by text processing + if key in ['Annotation_s_', 'Feature_s_'] and 'setup_analysis' in template_name: + return False + if isinstance(value, list): return True @@ -367,7 +447,7 @@ def should_normalize_as_list(key, value): return False # Skip known single-value parameters - if any(x in key_lower for x in ['single', 'one', 'first', 'second', 'primary']): + if any(x in key_lower for x in ['single', 'one', 'first', 'second', 'primary', 'centroid']): return False # Plural forms suggest lists @@ -417,7 +497,7 @@ def normalize_to_list(value): return [value] if value is not None else [] # Normalize list parameters -print("[Runner] Step 2: Normalizing list parameters") +print("[Runner] Step 3: Normalizing list parameters") list_count = 0 for key, value in list(params.items()): if should_normalize_as_list(key, value): @@ -434,23 +514,6 @@ for key, value in list(params.items()): if list_count > 0: print(f"[Runner] Normalized {list_count} list parameters") -# CRITICAL FIX: Handle single-element lists for coordinate columns -# These should be strings, not lists -coordinate_keys = ['X_Coordinate_Column', 'Y_Coordinate_Column', 'X_centroid', 'Y_centroid'] -for key in coordinate_keys: - if key in params: - value = params[key] - if isinstance(value, list) and len(value) == 1: - params[key] = value[0] - print(f"[Runner] Extracted single value from {key}: {value} -> {params[key]}") - -# Also check for any key ending with '_Column' that has a single-element list -for key in list(params.keys()): - if key.endswith('_Column') and isinstance(params[key], list) and len(params[key]) == 1: - original = params[key] - params[key] = params[key][0] - print(f"[Runner] Extracted single value from {key}: {original} -> {params[key]}") - # =========================================================================== # OUTPUTS HANDLING # =========================================================================== @@ -500,7 +563,11 @@ if 'DataFrames' in outputs: df_dir = outputs['DataFrames'] params['output_dir'] = df_dir params['Export_Dir'] = df_dir - params['Output_File'] = os.path.join(df_dir, f'{template_name}_output.csv') + # For load_csv, use a specific filename for the combined dataframe + if 'load_csv' in template_name: + params['Output_File'] = os.path.join(df_dir, 'combined_dataframe.csv') + else: + params['Output_File'] = os.path.join(df_dir, f'{template_name}_output.csv') if 'figures' in outputs: fig_dir = outputs['figures'] @@ -584,8 +651,80 @@ try: result = mod.run_from_json('params.runtime.json', **kwargs) print(f"[Runner] Template completed, returned: {type(result).__name__}") - # Handle different return types - if result is not None: + # =========================================================================== + # SPECIAL HANDLING FOR LOAD_CSV_FILES TEMPLATE + # =========================================================================== + if 'load_csv' in template_name: + print("[Runner] Special handling for load_csv_files template") + + # The template should return a DataFrame or save CSV files + if result is not None: + try: + import pandas as pd + + # If result is a DataFrame, save it directly + if hasattr(result, 'to_csv'): + output_path = os.path.join(outputs.get('DataFrames', 'dataframe_folder'), 'combined_dataframe.csv') + result.to_csv(output_path, index=False, header=True) + print(f"[Runner] Saved combined DataFrame to {output_path}") + + # If result is a dict of DataFrames, combine them + elif isinstance(result, dict): + dfs = [] + for name, df in result.items(): + if hasattr(df, 'to_csv'): + # Add a source column to track origin + df['_source_file'] = name + dfs.append(df) + + if dfs: + combined = pd.concat(dfs, ignore_index=True) + output_path = os.path.join(outputs.get('DataFrames', 'dataframe_folder'), 'combined_dataframe.csv') + combined.to_csv(output_path, index=False, header=True) + print(f"[Runner] Combined {len(dfs)} DataFrames into {output_path}") + except Exception as e: + print(f"[Runner] Could not combine DataFrames: {e}") + + # Check if CSV files were saved in the dataframe folder + df_dir = outputs.get('DataFrames', 'dataframe_folder') + if os.path.exists(df_dir): + csv_files = [f for f in os.listdir(df_dir) if f.endswith('.csv')] + + # If we have multiple CSV files but no combined_dataframe.csv, create it + if len(csv_files) > 1 and 'combined_dataframe.csv' not in csv_files: + try: + import pandas as pd + dfs = [] + for csv_file in csv_files: + filepath = os.path.join(df_dir, csv_file) + df = pd.read_csv(filepath) + df['_source_file'] = csv_file.replace('.csv', '') + dfs.append(df) + + combined = pd.concat(dfs, ignore_index=True) + output_path = os.path.join(df_dir, 'combined_dataframe.csv') + combined.to_csv(output_path, index=False, header=True) + print(f"[Runner] Combined {len(csv_files)} CSV files into {output_path}") + except Exception as e: + print(f"[Runner] Could not combine CSV files: {e}") + # If combination fails, just rename the first CSV + if csv_files: + src = os.path.join(df_dir, csv_files[0]) + dst = os.path.join(df_dir, 'combined_dataframe.csv') + shutil.copy2(src, dst) + print(f"[Runner] Copied {csv_files[0]} to combined_dataframe.csv") + + # If we have exactly one CSV file and it's not named combined_dataframe.csv, rename it + elif len(csv_files) == 1 and csv_files[0] != 'combined_dataframe.csv': + src = os.path.join(df_dir, csv_files[0]) + dst = os.path.join(df_dir, 'combined_dataframe.csv') + shutil.move(src, dst) + print(f"[Runner] Renamed {csv_files[0]} to combined_dataframe.csv") + + # =========================================================================== + # HANDLE OTHER RETURN TYPES + # =========================================================================== + elif result is not None: if isinstance(result, dict): print(f"[Runner] Template saved files: {list(result.keys())}") elif isinstance(result, tuple): @@ -612,7 +751,7 @@ try: elif hasattr(result, 'to_csv') and 'DataFrames' in outputs: df_path = os.path.join(outputs['DataFrames'], 'output.csv') - result.to_csv(df_path, index=True) + result.to_csv(df_path, index=False, header=True) print(f"[Runner] Saved DataFrame to {df_path}") elif hasattr(result, 'savefig') and 'figures' in outputs: @@ -655,7 +794,7 @@ for output_type, path in outputs.items(): if output_type == 'analysis': if os.path.exists(path): size = os.path.getsize(path) - print(f"[Runner] ✔ {output_type}: {path} ({size:,} bytes)") + print(f"[Runner] ✓ {output_type}: {path} ({size:,} bytes)") found_outputs = True else: print(f"[Runner] ✗ {output_type}: NOT FOUND") @@ -663,7 +802,7 @@ for output_type, path in outputs.items(): if os.path.exists(path) and os.path.isdir(path): files = os.listdir(path) if files: - print(f"[Runner] ✔ {output_type}: {len(files)} files") + print(f"[Runner] ✓ {output_type}: {len(files)} files") for f in files[:3]: print(f"[Runner] - {f}") if len(files) > 3: diff --git a/galaxy_tools/spac_setup_analysis/spac_setup_analysis.xml b/galaxy_tools/spac_setup_analysis/spac_setup_analysis.xml index 495887e7..fefc6f95 100644 --- a/galaxy_tools/spac_setup_analysis/spac_setup_analysis.xml +++ b/galaxy_tools/spac_setup_analysis/spac_setup_analysis.xml @@ -1,66 +1,121 @@ - Convert the pre-processed dataset to the analysis object for downstream analysis. + Set up an analysis data object for downstream processing. + + + nciccbr/spac:v1 + + + + python3 + + + tool_stdout.txt && - - nciccbr/spac:v1 - + ## Run the universal wrapper + bash $__tool_directory__/run_spac_template.sh "$params_json" setup_analysis + ]]> + + + + + + + + - - python3 - + + + - tool_stdout.txt && - - ## Run the universal wrapper (template name without .py extension) - bash $__tool_directory__/run_spac_template.sh "$params_json" setup_analysis - ]]> + - - - - + + + + - - - - - - - - - - + + + + + + + + + + + - - - - + **Important**: Column names must be entered EXACTLY as they appear in your data, including: + - Special characters (e.g., CD45+, CD45_positive) + - Spaces (e.g., "Cell Type") + - Case sensitivity (e.g., CD3 vs cd3) + + **Inputs:** + - X/Y centroid columns: Spatial coordinate columns + - Annotation columns: Metadata columns + - Feature columns: Measurement/marker columns + + **Column Entry Format:** + - Multiple columns: Use commas or enter one per line + - Single column: Just type the name + + **Feature Selection:** + Use either Feature column names OR Feature Regex Pattern, not both: + - Direct names: List specific columns + - Regex pattern: Match multiple columns (e.g., "^CD" for all CD markers) - + **Output:** + - Analysis Object: A pickle file containing the configured analysis for downstream tools + - Parameters Used: JSON file documenting the configuration + - Execution Log: Detailed log of the setup process - - - @misc{spac_toolkit, - author = {FNLCR DMAP Team}, - title = {SPAC: SPAtial single-Cell analysis}, - year = {2024}, - url = {https://github.com/FNLCR-DMAP/SCSAWorkflow} - } - - - \ No newline at end of file + This tool is part of the SPAC (SPAtial single-Cell analysis) toolkit. + ]]> + + + +@misc{spac_toolkit, + author = {FNLCR DMAP Team}, + title = {SPAC: SPAtial single-Cell analysis}, + year = {2024}, + url = {https://github.com/FNLCR-DMAP/SCSAWorkflow} +} + + + \ No newline at end of file diff --git a/galaxy_tools/spac_zscore_normalization/spac_zscore_normalization.xml b/galaxy_tools/spac_zscore_normalization/spac_zscore_normalization.xml index 003c8089..52be678c 100644 --- a/galaxy_tools/spac_zscore_normalization/spac_zscore_normalization.xml +++ b/galaxy_tools/spac_zscore_normalization/spac_zscore_normalization.xml @@ -23,18 +23,22 @@ - + - + - - - + + + + Date: Thu, 16 Oct 2025 11:16:20 -0400 Subject: [PATCH 089/102] init benchmark script --- scripts/visualization_benchmark.py | 798 +++++++++++++++++++++++++++++ 1 file changed, 798 insertions(+) create mode 100644 scripts/visualization_benchmark.py diff --git a/scripts/visualization_benchmark.py b/scripts/visualization_benchmark.py new file mode 100644 index 00000000..5c1aebcb --- /dev/null +++ b/scripts/visualization_benchmark.py @@ -0,0 +1,798 @@ +#!/usr/bin/env python3 +""" +Visualization Benchmark Script +=============================== + +A command-line tool for benchmarking visualization functions from the SPAC +package. This script measures the performance of various plotting functions +with different dataset sizes and configurations. + +Usage: + # Benchmark with a custom dataset + python visualization_benchmark.py --dataset path/to/data.pkl --visualizations boxplot histogram + + # Benchmark with generated random data + python visualization_benchmark.py --min-datapoints 1000 --max-datapoints 10000 \ + --increment 1000 --visualizations both --output-plot + +Author: SPAC Development Team +Date: October 2025 +""" + +import argparse +import sys +import time +import pickle +from pathlib import Path +from typing import Dict, List, Tuple + +import numpy as np +import pandas as pd +import anndata as ad +import matplotlib.pyplot as plt +from sklearn.datasets import make_blobs +from sklearn.preprocessing import StandardScaler + +# Import SPAC visualization functions +from spac.visualization import boxplot, boxplot_interactive + + +def parse_arguments() -> argparse.Namespace: + """ + Parse command-line arguments for the benchmark script. + + Returns + ------- + argparse.Namespace + Parsed command-line arguments containing benchmark configuration. + """ + parser = argparse.ArgumentParser( + description="Benchmark visualization functions from the SPAC package", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Benchmark with custom dataset + %(prog)s --dataset data.pkl --visualizations boxplot histogram --output-plot + + # Benchmark with a single dataset size + %(prog)s --size 5000 --visualizations both --output-plot + + # Benchmark with generated data range + %(prog)s --min-datapoints 1000 --max-datapoints 5000 --increment 1000 \\ + --visualizations both --output-plot + + # Run only histogram benchmarks with single size + %(prog)s --size 2000 --visualizations histogram + """ + ) + + # Dataset input options + data_group = parser.add_argument_group("Dataset Options") + data_group.add_argument( + "--dataset", + type=str, + default=None, + metavar="PATH", + help="Path to input dataset file (pickle or anndata-readable format). " + "If not provided, random datasets will be generated." + ) + + # Random data generation options + random_group = parser.add_argument_group( + "Random Data Generation Options", + "Used when --dataset is not provided" + ) + random_group.add_argument( + "--size", + type=int, + default=None, + metavar="N", + help="Single dataset size to benchmark. If specified, overrides " + "min/max/increment options." + ) + random_group.add_argument( + "--min-datapoints", + type=int, + default=1000, + metavar="N", + help="Minimum number of datapoints for generated datasets (default: 1000). " + "Ignored if --size is specified." + ) + random_group.add_argument( + "--max-datapoints", + type=int, + default=10000, + metavar="N", + help="Maximum number of datapoints for generated datasets (default: 10000). " + "Ignored if --size is specified." + ) + random_group.add_argument( + "--increment", + type=int, + default=1000, + metavar="N", + help="Increment step for datapoint sizes (default: 1000). " + "Ignored if --size is specified." + ) + + # Visualization options + viz_group = parser.add_argument_group("Visualization Options") + viz_group.add_argument( + "--visualizations", + type=str, + nargs="+", + choices=["boxplot", "histogram", "both"], + default=["both"], + metavar="TYPE", + help="Which visualizations to benchmark: boxplot, histogram, or both " + "(default: both)" + ) + + # Plot parameter options + plot_group = parser.add_argument_group( + "Plot Parameters", + "Optional parameters for visualization functions. If not specified, " + "sensible defaults will be auto-discovered from the dataset." + ) + plot_group.add_argument( + "--annotation", + type=str, + default=None, + metavar="NAME", + help="Annotation column to use for grouping (e.g., cell_type). " + "If not provided, the first categorical annotation will be used." + ) + plot_group.add_argument( + "--features", + type=str, + nargs="+", + default=None, + metavar="NAME", + help="Feature(s) to plot. Provide one or more feature names separated by spaces. " + "If not provided, the first 3 features will be used." + ) + plot_group.add_argument( + "--layer", + type=str, + default=None, + metavar="NAME", + help="Layer to use for plotting (e.g., normalized, log_transformed). " + "If not provided, the main data matrix (adata.X) will be used." + ) + + # Output options + output_group = parser.add_argument_group("Output Options") + output_group.add_argument( + "--output-plot", + action="store_true", + help="Generate and save a plot comparing benchmark results" + ) + output_group.add_argument( + "--output-dir", + type=str, + default="benchmark_results", + metavar="DIR", + help="Directory to save benchmark results (default: benchmark_results)" + ) + + # Parse arguments + args = parser.parse_args() + + # Validate arguments + _validate_arguments(args) + + return args + + +def _validate_arguments(args: argparse.Namespace) -> None: + """ + Validate the parsed command-line arguments. + + Parameters + ---------- + args : argparse.Namespace + Parsed command-line arguments to validate. + + Raises + ------ + ValueError + If any argument validation fails. + SystemExit + If validation fails, exits with error message. + """ + # Validate dataset path if provided + if args.dataset is not None: + dataset_path = Path(args.dataset) + if not dataset_path.exists(): + print(f"Error: Dataset file not found: {args.dataset}", file=sys.stderr) + sys.exit(1) + if not dataset_path.is_file(): + print(f"Error: Dataset path is not a file: {args.dataset}", file=sys.stderr) + sys.exit(1) + + # Validate random data generation parameters + if args.dataset is None: + if args.size is not None: + # Single size mode + if args.size <= 0: + print("Error: --size must be positive", file=sys.stderr) + sys.exit(1) + else: + # Range mode + if args.min_datapoints <= 0: + print("Error: --min-datapoints must be positive", file=sys.stderr) + sys.exit(1) + if args.max_datapoints <= 0: + print("Error: --max-datapoints must be positive", file=sys.stderr) + sys.exit(1) + if args.min_datapoints > args.max_datapoints: + print("Error: --min-datapoints cannot exceed --max-datapoints", + file=sys.stderr) + sys.exit(1) + if args.increment <= 0: + print("Error: --increment must be positive", file=sys.stderr) + sys.exit(1) + + # Normalize "both" in visualizations list + if "both" in args.visualizations: + args.visualizations = ["boxplot", "histogram"] + else: + # Remove duplicates while preserving order + args.visualizations = list(dict.fromkeys(args.visualizations)) + + +def _discover_plot_parameters(adata: ad.AnnData, args: argparse.Namespace) -> dict: + """ + Auto-discover sensible default parameters for plotting from the dataset. + + Parameters + ---------- + adata : ad.AnnData + The AnnData object to discover parameters from. + args : argparse.Namespace + Command-line arguments that may contain user-specified overrides. + + Returns + ------- + dict + Dictionary containing discovered/selected parameters: + - annotation: str or None + - features: list of str + - layer: str or None + """ + params = {} + + # Discover or use specified annotation + if args.annotation is not None: + # Validate user-specified annotation + if args.annotation not in adata.obs.columns: + print(f"Error: Annotation '{args.annotation}' not found in dataset.", + file=sys.stderr) + print(f"Available annotations: {', '.join(adata.obs.columns.tolist())}", + file=sys.stderr) + sys.exit(1) + params['annotation'] = args.annotation + print(f" Using specified annotation: {args.annotation}") + else: + # Auto-discover: use first categorical column + categorical_cols = [col for col in adata.obs.columns + if pd.api.types.is_categorical_dtype(adata.obs[col]) or + adata.obs[col].dtype == 'object'] + if categorical_cols: + params['annotation'] = categorical_cols[0] + print(f" Auto-discovered annotation: {params['annotation']}") + print(f" (Available: {', '.join(categorical_cols)})") + else: + params['annotation'] = None + print(" No categorical annotations found, will use None") + + # Discover or use specified features + if args.features is not None: + # Validate user-specified features + invalid_features = [f for f in args.features if f not in adata.var_names] + if invalid_features: + print(f"Error: Features not found in dataset: {', '.join(invalid_features)}", + file=sys.stderr) + print(f"Available features: {', '.join(adata.var_names[:10].tolist())}" + f"{'...' if len(adata.var_names) > 10 else ''}", + file=sys.stderr) + sys.exit(1) + params['features'] = args.features + print(f" Using specified features: {', '.join(args.features)}") + else: + # Auto-discover: use first 3 features (or all if fewer than 3) + n_features = min(3, len(adata.var_names)) + params['features'] = adata.var_names[:n_features].tolist() + print(f" Auto-discovered features: {', '.join(params['features'])}") + if len(adata.var_names) > 3: + print(f" (Total available: {len(adata.var_names)})") + + # Discover or use specified layer + if args.layer is not None: + # Validate user-specified layer + if args.layer not in adata.layers.keys(): + print(f"Error: Layer '{args.layer}' not found in dataset.", + file=sys.stderr) + available = ', '.join(adata.layers.keys()) if adata.layers.keys() else 'None' + print(f"Available layers: {available}", file=sys.stderr) + sys.exit(1) + params['layer'] = args.layer + print(f" Using specified layer: {args.layer}") + else: + # Auto-discover: use first layer if available, otherwise None (main matrix) + if len(adata.layers.keys()) > 0: + params['layer'] = list(adata.layers.keys())[0] + print(f" Auto-discovered layer: {params['layer']}") + print(f" (Available: {', '.join(adata.layers.keys())})") + else: + params['layer'] = None + print(" No layers found, will use main matrix (adata.X)") + + return params + + +def load_dataset(dataset_path: str) -> ad.AnnData: + """ + Load a dataset from file, supporting both pickle and anndata formats. + + Parameters + ---------- + dataset_path : str + Path to the dataset file. Supports pickle files (.pkl, .pickle) + and any format readable by anndata (e.g., .h5ad, .zarr, .loom). + + Returns + ------- + ad.AnnData + Loaded AnnData object containing the dataset. + + Raises + ------ + ValueError + If the file format is not supported or loading fails. + SystemExit + If the loaded object is not an AnnData instance. + """ + file_path = Path(dataset_path) + file_suffix = file_path.suffix.lower() + + print(f"Loading dataset from: {dataset_path}") + + try: + # Try loading as pickle file first for .pkl and .pickle extensions + if file_suffix in ['.pkl', '.pickle']: + print(" Detected pickle format, loading with pickle...") + with open(file_path, 'rb') as f: + adata = pickle.load(f) + + # Ensure the loaded object is an AnnData instance + if not isinstance(adata, ad.AnnData): + print(f"Error: Pickle file does not contain an AnnData object. " + f"Found type: {type(adata).__name__}", file=sys.stderr) + sys.exit(1) + + else: + # Try loading with anndata for other formats + print(f" Attempting to load with anndata...") + adata = ad.read(dataset_path) + + # Validate that we have a proper AnnData object + if not isinstance(adata, ad.AnnData): + print(f"Error: Loaded object is not an AnnData instance. " + f"Found type: {type(adata).__name__}", file=sys.stderr) + sys.exit(1) + + # Print basic dataset information + print(f" Successfully loaded dataset:") + print(f" Observations (cells): {adata.n_obs}") + print(f" Variables (features): {adata.n_vars}") + if adata.obs.columns.size > 0: + print(f" Annotations: {', '.join(adata.obs.columns[:5].tolist())}" + f"{'...' if len(adata.obs.columns) > 5 else ''}") + print() + + return adata + + except FileNotFoundError: + print(f"Error: File not found: {dataset_path}", file=sys.stderr) + sys.exit(1) + except pickle.UnpicklingError as e: + print(f"Error: Failed to unpickle file: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error: Failed to load dataset: {e}", file=sys.stderr) + print(f" Supported formats: pickle (.pkl, .pickle) or anndata-readable " + f"formats (.h5ad, .zarr, .loom, etc.)", file=sys.stderr) + sys.exit(1) + + +def generate_random_dataset(n_obs: int, random_state: int = 42) -> ad.AnnData: + """ + Generate a random AnnData object with realistic clustering for benchmarking. + + Creates a synthetic dataset with 5 features, 5 annotations, and 3 data layers. + Uses sklearn's make_blobs to generate data with natural clustering patterns + rather than pure noise. + + Parameters + ---------- + n_obs : int + Number of observations (cells/datapoints) to generate. + random_state : int, optional + Random seed for reproducibility. Default is 42. + + Returns + ------- + ad.AnnData + Generated AnnData object with the following structure: + - X: Main feature matrix (n_obs × 5 features) + - obs: DataFrame with 5 categorical annotations + - var: Feature names (marker_1 through marker_5) + - layers: 3 transformed versions of the data + (normalized, log_transformed, scaled) + """ + np.random.seed(random_state) + + # Generate base data with natural clustering using make_blobs + # Create 5 features with 5 cluster centers for realistic structure + n_features = 5 + n_centers = 5 + + X, cluster_labels = make_blobs( + n_samples=n_obs, + n_features=n_features, + centers=n_centers, + cluster_std=1.5, + random_state=random_state + ) + + # Make values positive (common in biological data) and add some variation + X = np.abs(X) + np.random.exponential(scale=2.0, size=X.shape) + + # Create feature names + feature_names = [f"marker_{i+1}" for i in range(n_features)] + + # Create 5 annotations with varying numbers of unique values + # Assign based on clusters and add some randomness for realism + + # Annotation 1: cell_type (5 categories) + cell_types = [f"Type_{chr(65+i)}" for i in range(5)] # Type_A, Type_B, etc. + cell_type = np.array([cell_types[i % 5] for i in cluster_labels]) + + # Annotation 2: phenotype (4 categories) - partially correlated with clusters + phenotypes = [f"Pheno_{i+1}" for i in range(4)] + phenotype = np.array([phenotypes[i % 4] for i in cluster_labels]) + # Add some randomness to ~20% of assignments + random_mask = np.random.random(n_obs) < 0.2 + phenotype[random_mask] = np.random.choice(phenotypes, size=random_mask.sum()) + + # Annotation 3: region (3 categories) - more random + regions = ["Region_X", "Region_Y", "Region_Z"] + region = np.random.choice(regions, size=n_obs) + + # Annotation 4: batch (3 categories) - random assignment + batches = ["Batch_1", "Batch_2", "Batch_3"] + batch = np.random.choice(batches, size=n_obs) + + # Annotation 5: treatment (2 categories) - binary, roughly balanced + treatments = ["Control", "Treated"] + treatment = np.random.choice(treatments, size=n_obs, p=[0.5, 0.5]) + + # Create observations DataFrame + obs = pd.DataFrame({ + 'cell_type': pd.Categorical(cell_type), + 'phenotype': pd.Categorical(phenotype), + 'region': pd.Categorical(region), + 'batch': pd.Categorical(batch), + 'treatment': pd.Categorical(treatment) + }) + + # Create the base AnnData object + adata = ad.AnnData(X=X, obs=obs) + adata.var_names = feature_names + + # Create 3 layers with different transformations + + # Layer 1: Normalized (min-max normalization per feature) + X_normalized = np.zeros_like(X) + for i in range(n_features): + feature_min = X[:, i].min() + feature_max = X[:, i].max() + X_normalized[:, i] = (X[:, i] - feature_min) / (feature_max - feature_min) + adata.layers['normalized'] = X_normalized + + # Layer 2: Log-transformed (log1p to handle zeros) + adata.layers['log_transformed'] = np.log1p(X) + + # Layer 3: Scaled (standardized with mean=0, std=1) + scaler = StandardScaler() + adata.layers['scaled'] = scaler.fit_transform(X) + + return adata + + +def benchmark_boxplot( + adata: ad.AnnData, + annotation: str, + features: List[str], + layer: str = None +) -> Tuple[float, dict]: + """ + Benchmark the standard boxplot function. + + Parameters + ---------- + adata : ad.AnnData + The AnnData object to plot. + annotation : str + Annotation column for grouping. + features : List[str] + List of features to plot. + layer : str, optional + Layer to use for plotting. + + Returns + ------- + tuple + (execution_time, result_dict) where result_dict contains the plot outputs. + """ + # Close any existing plots to avoid memory issues + plt.close('all') + + start_time = time.time() + + try: + # Call boxplot function + fig, ax, df = boxplot( + adata, + annotation=annotation, + features=features, + layer=layer + ) + + execution_time = time.time() - start_time + + # Clean up to free memory + plt.close(fig) + + return execution_time, {'fig': fig, 'ax': ax, 'df': df} + + except Exception as e: + execution_time = time.time() - start_time + print(f" Error in boxplot: {e}") + return execution_time, {'error': str(e)} + + +def benchmark_boxplot_interactive( + adata: ad.AnnData, + annotation: str, + features: List[str], + layer: str = None +) -> Tuple[float, dict]: + """ + Benchmark the interactive boxplot function. + + Parameters + ---------- + adata : ad.AnnData + The AnnData object to plot. + annotation : str + Annotation column for grouping. + features : List[str] + List of features to plot. + layer : str, optional + Layer to use for plotting. + + Returns + ------- + tuple + (execution_time, result_dict) where result_dict contains the plot outputs. + """ + # Close any existing plots to avoid memory issues + plt.close('all') + + start_time = time.time() + + try: + # Call boxplot_interactive function with showfliers='downsample' + result = boxplot_interactive( + adata, + annotation=annotation, + features=features, + layer=layer, + showfliers='downsample' + ) + + execution_time = time.time() - start_time + + return execution_time, result + + except Exception as e: + execution_time = time.time() - start_time + print(f" Error in boxplot_interactive: {e}") + return execution_time, {'error': str(e)} + + +def run_boxplot_benchmarks( + datasets: List[ad.AnnData], + plot_params: dict +) -> pd.DataFrame: + """ + Run boxplot benchmarks on all provided datasets. + + Parameters + ---------- + datasets : List[ad.AnnData] + List of datasets to benchmark on. + plot_params : dict + Dictionary containing plot parameters (annotation, features, layer). + + Returns + ------- + pd.DataFrame + DataFrame containing benchmark results with columns: + - n_obs: number of observations + - boxplot_time: execution time for boxplot + - boxplot_interactive_time: execution time for boxplot_interactive + - speedup_factor: ratio of times (boxplot / boxplot_interactive) + """ + print("Running boxplot benchmarks...") + print("=" * 70) + + results = [] + + for i, adata in enumerate(datasets, 1): + n_obs = adata.n_obs + print(f"\nDataset {i}/{len(datasets)}: {n_obs} observations") + + # Benchmark standard boxplot + print(" Testing boxplot...") + boxplot_time, boxplot_result = benchmark_boxplot( + adata, + annotation=plot_params['annotation'], + features=plot_params['features'], + layer=plot_params['layer'] + ) + print(f" Time: {boxplot_time:.4f} seconds") + + # Benchmark interactive boxplot + print(" Testing boxplot_interactive...") + interactive_time, interactive_result = benchmark_boxplot_interactive( + adata, + annotation=plot_params['annotation'], + features=plot_params['features'], + layer=plot_params['layer'] + ) + print(f" Time: {interactive_time:.4f} seconds") + + # Calculate speedup factor + if interactive_time > 0: + speedup = boxplot_time / interactive_time + print(f" Speedup factor: {speedup:.2f}x " + f"({'boxplot_interactive' if speedup > 1 else 'boxplot'} is faster)") + else: + speedup = None + + results.append({ + 'n_obs': n_obs, + 'boxplot_time': boxplot_time, + 'boxplot_interactive_time': interactive_time, + 'speedup_factor': speedup + }) + + print("\n" + "=" * 70) + print("Benchmark complete!") + + return pd.DataFrame(results) + + +def main(): + """ + Main entry point for the benchmark script. + + Orchestrates the benchmarking process including argument parsing, + data loading/generation, running benchmarks, and outputting results. + """ + # Parse command-line arguments + args = parse_arguments() + + print("=" * 70) + print("SPAC Visualization Benchmark") + print("=" * 70) + print() + + # Display configuration + print(f"Configuration:") + print(f" Dataset: {args.dataset if args.dataset else 'Generated'}") + if args.dataset is None: + if args.size is not None: + print(f" Dataset size: {args.size}") + else: + print(f" Datapoint range: {args.min_datapoints} - {args.max_datapoints}") + print(f" Increment: {args.increment}") + print(f" Visualizations: {', '.join(args.visualizations)}") + print(f" Output plot: {args.output_plot}") + print(f" Output directory: {args.output_dir}") + print() + + # Step 1: Load or generate datasets + if args.dataset is not None: + # Load user-provided dataset + adata = load_dataset(args.dataset) + datasets = [adata] + else: + # Generate random datasets for benchmarking + print("Generating random datasets for benchmarking...") + datasets = [] + + if args.size is not None: + # Single size mode + dataset_sizes = [args.size] + else: + # Range mode + dataset_sizes = range( + args.min_datapoints, + args.max_datapoints + 1, + args.increment + ) + + for size in dataset_sizes: + print(f" Generating dataset with {size} observations...") + adata = generate_random_dataset(n_obs=size, random_state=42) + datasets.append(adata) + + print(f" Successfully generated {len(datasets)} dataset(s)") + if args.size is not None: + print(f" Size: {args.size}") + else: + print(f" Size range: {args.min_datapoints} to {args.max_datapoints}") + print() + + # Step 2: Auto-discover plot parameters from the first dataset + print("Discovering plot parameters...") + plot_params = _discover_plot_parameters(datasets[0], args) + print() + + print(f"Plot parameters to be used:") + print(f" Annotation: {plot_params['annotation']}") + print(f" Features: {', '.join(plot_params['features'])}") + print(f" Layer: {plot_params['layer']}") + print() + + # Step 3: Run benchmarks based on selected visualizations + all_results = {} + + if 'boxplot' in args.visualizations: + # Run boxplot vs boxplot_interactive benchmark + boxplot_results = run_boxplot_benchmarks(datasets, plot_params) + all_results['boxplot'] = boxplot_results + + # Display summary + print("\nBoxplot Benchmark Summary:") + print(boxplot_results.to_string(index=False)) + print() + + # TODO: Add histogram benchmarks when requested + + # Step 4: Save results to output directory + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + for viz_type, results_df in all_results.items(): + output_file = output_dir / f"{viz_type}_benchmark_results.csv" + results_df.to_csv(output_file, index=False) + print(f"Results saved to: {output_file}") + + # Step 5: Generate comparison plots if requested + if args.output_plot and len(all_results) > 0: + print("\nGenerating comparison plots...") + # TODO: Implement plot generation + print(" Plot generation not yet implemented") + + print("\n" + "=" * 70) + print("Benchmark execution complete!") + print("=" * 70) + + +if __name__ == "__main__": + main() From 4d2e3d722472e4a1808e936bd6267cc59e709a55 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Fri, 17 Oct 2025 02:14:43 -0400 Subject: [PATCH 090/102] feat: add refactored galaxy tools --- .../nidap_to_galaxy_synthesizer.py | 569 ++++++++++++++++++ .../refactor_tools/run_spac_template.sh | 27 + .../spac_arcsinh_normalization.xml | 69 +++ galaxy_tools/refactor_tools/spac_boxplot.xml | 85 +++ .../refactor_tools/spac_galaxy_runner.py | 515 ++++++++++++++++ .../refactor_tools/spac_load_csv_files.xml | 75 +++ .../refactor_tools/spac_setup_analysis.xml | 71 +++ 7 files changed, 1411 insertions(+) create mode 100644 galaxy_tools/refactor_tools/nidap_to_galaxy_synthesizer.py create mode 100644 galaxy_tools/refactor_tools/run_spac_template.sh create mode 100644 galaxy_tools/refactor_tools/spac_arcsinh_normalization.xml create mode 100644 galaxy_tools/refactor_tools/spac_boxplot.xml create mode 100644 galaxy_tools/refactor_tools/spac_galaxy_runner.py create mode 100644 galaxy_tools/refactor_tools/spac_load_csv_files.xml create mode 100644 galaxy_tools/refactor_tools/spac_setup_analysis.xml diff --git a/galaxy_tools/refactor_tools/nidap_to_galaxy_synthesizer.py b/galaxy_tools/refactor_tools/nidap_to_galaxy_synthesizer.py new file mode 100644 index 00000000..d695dc7d --- /dev/null +++ b/galaxy_tools/refactor_tools/nidap_to_galaxy_synthesizer.py @@ -0,0 +1,569 @@ +#!/usr/bin/env python3 +""" +Generalized NIDAP to Galaxy synthesizer - Production Version v11 +- No hardcoded tool-specific logic +- Blueprint-driven for all tools +- Handles multiple files/columns via blueprint flags +- FIXED: Use 'binary' instead of 'pickle' for Galaxy compatibility +- FIXED: Use 'set -eu' instead of 'set -euo pipefail' for broader shell compatibility +- FIXED: Pass outputs spec as environment variable to avoid encoding issues +- FIXED: Method signature for build_command_section +""" + +import argparse +import json +import re +import shutil +from pathlib import Path +from typing import Dict, List, Tuple + +class GeneralizedNIDAPToGalaxySynthesizer: + + def __init__(self, docker_image: str = "nciccbr/spac:v1"): + self.docker_image = docker_image + self.galaxy_profile = "24.2" + self.wrapper_script = Path('run_spac_template.sh') + self.runner_script = Path('spac_galaxy_runner.py') + + def slugify(self, name: str) -> str: + """Convert name to valid Galaxy tool ID component""" + s = re.sub(r'\[.*?\]', '', name).strip() + s = s.lower() + s = re.sub(r'\s+', '_', s) + s = re.sub(r'[^a-z0-9_]+', '', s) + s = re.sub(r'_+', '_', s) + return s.strip('_') + + def escape_xml(self, text: str, is_attribute: bool = True) -> str: + """Escape XML special characters""" + if text is None: + return "" + text = str(text) + text = text.replace('&', '&') + text = text.replace('<', '<') + text = text.replace('>', '>') + if is_attribute: + text = text.replace('"', '"') + text = text.replace("'", ''') + return text + + def clean_description(self, description: str) -> str: + """Clean NIDAP-specific content from descriptions""" + if not description: + return "" + + desc = str(description).replace('\r\n', '\n').replace('\r', '\n') + desc = re.sub(r'\[DUET\s*Documentation\]\([^)]+\)', '', desc, flags=re.IGNORECASE) + desc = re.sub(r'Please refer to\s+(?:,?\s*and\s*)+', '', desc, flags=re.IGNORECASE) + desc = re.sub(r'\\(?=\s*(?:\n|$))', '', desc) + desc = re.sub(r'[ \t]{2,}', ' ', desc) + desc = re.sub(r'\n{3,}', '\n\n', desc) + + return desc.strip() + + def determine_input_format(self, dataset: Dict, tool_name: str) -> str: + """ + Determine the correct format for an input dataset. + Simple mapping based on dataType field. + Uses 'binary' instead of 'pickle' for Galaxy compatibility. + """ + data_type = dataset.get('dataType', '').upper() + + # Handle comma-separated types (e.g., "CSV, Tabular") + data_types = [dt.strip() for dt in data_type.split(',')] + + # Check for CSV/Tabular types + if any(dt in ['CSV', 'TABULAR', 'TSV', 'TXT'] for dt in data_types): + return 'csv,tabular,tsv,txt' + + # DataFrame types + if any('DATAFRAME' in dt for dt in data_types): + return 'csv,tabular,tsv,txt' + + # AnnData/H5AD types + if any(dt in ['ANNDATA', 'H5AD', 'HDF5'] for dt in data_types): + return 'h5ad,h5,hdf5' + + # Pickle - use 'binary' for Galaxy compatibility + if any('PICKLE' in dt for dt in data_types): + return 'binary' + + # PYTHON_TRANSFORM_INPUT - default to binary (analysis objects) + if 'PYTHON_TRANSFORM_INPUT' in data_type: + return 'h5ad,binary' # Use binary instead of pickle + + # Default fallback + return 'h5ad,binary' # Use binary instead of pickle + + def build_inputs_section(self, blueprint: Dict, tool_name: str) -> Tuple[List[str], List[str]]: + """Build inputs from blueprint - generalized for all tools""" + lines = [] + multiple_file_inputs = [] # Track which inputs accept multiple files + + # Handle input datasets + for dataset in blueprint.get('inputDatasets', []): + name = dataset.get('key', 'input_data') + label = self.escape_xml(dataset.get('displayName', 'Input Data')) + desc = self.escape_xml(self.clean_description(dataset.get('description', ''))) + + # Determine format - now simpler with direct dataType mapping + formats = self.determine_input_format(dataset, tool_name) + + # Check if multiple files allowed (from blueprint) + is_multiple = dataset.get('isMultiple', False) + + if is_multiple: + multiple_file_inputs.append(name) + lines.append( + f' ' + ) + else: + lines.append( + f' ' + ) + + # Handle explicit column definitions from 'columns' schema + for col in blueprint.get('columns', []): + key = col.get('key') + if not key: + continue + + label = self.escape_xml(col.get('displayName', key)) + desc = self.escape_xml(col.get('description', '')) + # isMulti can be True, False, or None (None means False) + is_multi = col.get('isMulti') == True + + # Use text inputs for column names + if is_multi: + lines.append( + f' ' + ) + else: + lines.append( + f' ' + ) + + # Handle regular parameters + for param in blueprint.get('parameters', []): + key = param.get('key') + if not key: + continue + + label = self.escape_xml(param.get('displayName', key)) + desc = self.escape_xml(self.clean_description(param.get('description', ''))) + param_type = param.get('paramType', 'STRING').upper() + default = param.get('defaultValue', '') + is_optional = param.get('isOptional', False) + + # Add optional attribute if needed + optional_attr = ' optional="true"' if is_optional else '' + + if param_type == 'BOOLEAN': + checked = 'true' if str(default).strip().lower() == 'true' else 'false' + lines.append( + f' ' + ) + + elif param_type == 'INTEGER': + lines.append( + f' ' + ) + + elif param_type in ['NUMBER', 'FLOAT']: + lines.append( + f' ' + ) + + elif param_type == 'SELECT': + options = param.get('paramValues', []) + lines.append(f' ') + for opt in options: + selected = ' selected="true"' if str(opt) == str(default) else '' + opt_escaped = self.escape_xml(str(opt)) + lines.append(f' ') + lines.append(' ') + + elif param_type == 'LIST': + # Handle LIST type parameters - convert list to simple string + if isinstance(default, list): + # Filter out empty strings and join + filtered = [str(x) for x in default if x and str(x).strip()] + default = ', '.join(filtered) if filtered else '' + elif default == '[""]' or default == "['']" or default == '[]': + # Handle common empty list representations + default = '' + lines.append( + f' ' + ) + + else: # STRING + lines.append( + f' ' + ) + + return lines, multiple_file_inputs + + def build_outputs_section(self, outputs: Dict) -> List[str]: + """Build outputs section based on blueprint specification""" + lines = [] + + for output_type, output_path in outputs.items(): + + # Determine if single file or collection + is_collection = (output_path.endswith('_folder') or + output_path.endswith('_dir')) + + if not is_collection: + # Single file output + if output_type == 'analysis': + if '.h5ad' in output_path: + fmt = 'h5ad' + elif '.pickle' in output_path or '.pkl' in output_path: + fmt = 'binary' # Use binary instead of pickle + else: + fmt = 'binary' + + lines.append( + f' ' + ) + + elif output_type == 'DataFrames' and (output_path.endswith('.csv') or output_path.endswith('.tsv')): + # Single DataFrame file output + fmt = 'csv' if output_path.endswith('.csv') else 'tabular' + lines.append( + f' ' + ) + + elif output_type == 'figure': + ext = output_path.split('.')[-1] if '.' in output_path else 'png' + lines.append( + f' ' + ) + + elif output_type == 'html': + lines.append( + f' ' + ) + + else: + # Collection outputs + if output_type == 'DataFrames': + lines.append( + ' ' + ) + lines.append(f' ') + lines.append(f' ') + lines.append(' ') + + elif output_type == 'figures': + lines.append( + ' ' + ) + lines.append(f' ') + lines.append(f' ') + lines.append(f' ') + lines.append(' ') + + elif output_type == 'html': + lines.append( + ' ' + ) + lines.append(f' ') + lines.append(' ') + + # Debug outputs + lines.append(' ') + lines.append(' ') + lines.append(' ') + + return lines + + def build_command_section(self, tool_name: str, blueprint: Dict, multiple_file_inputs: List[str], outputs_spec: Dict) -> str: + """Build command section - generalized for all tools + FIXED: Use 'set -eu' instead of 'set -euo pipefail' for broader shell compatibility + FIXED: Pass outputs spec as environment variable to avoid encoding issues + """ + + # Convert outputs spec to JSON string + outputs_json = json.dumps(outputs_spec) + + # Check if any inputs accept multiple files + has_multiple_files = len(multiple_file_inputs) > 0 + + if has_multiple_files: + # Generate file copying logic for each multiple input + copy_sections = [] + for input_name in multiple_file_inputs: + # Use double curly braces to escape them in f-strings + copy_sections.append(f''' + ## Create directory for {input_name} + mkdir -p {input_name}_dir && + + ## Copy files to directory with original names + #for $i, $file in enumerate(${input_name}) + cp '${{file}}' '{input_name}_dir/${{file.name}}' && + #end for''') + + copy_logic = ''.join(copy_sections) + + command_section = f''' &2 && + cat "$params_json" >&2 && + echo "==================" >&2 && + + ## Save snapshot + cp "$params_json" params_snapshot.json && + + ## Run wrapper + bash $__tool_directory__/run_spac_template.sh "$params_json" "{tool_name}" + ]]>''' + else: + # Standard command for single-file inputs + command_section = f''' &2 && + cat "$params_json" >&2 && + echo "==================" >&2 && + + ## Save snapshot + cp "$params_json" params_snapshot.json && + + ## Run wrapper + bash $__tool_directory__/run_spac_template.sh "$params_json" "{tool_name}" + ]]>''' + + return command_section + + def get_template_filename(self, title: str, tool_name: str) -> str: + """Get the correct template filename""" + # Check if there's a custom mapping in the blueprint + # Otherwise use standard naming convention + if title == 'Load CSV Files' or tool_name == 'load_csv_files': + return 'load_csv_files_with_config.py' + else: + return f'{tool_name}_template.py' + + def generate_tool(self, json_path: Path, output_dir: Path) -> Dict: + """Generate Galaxy tool from NIDAP JSON blueprint""" + + with open(json_path, 'r') as f: + blueprint = json.load(f) + + title = blueprint.get('title', 'Unknown Tool') + clean_title = re.sub(r'\[.*?\]', '', title).strip() + + tool_name = self.slugify(clean_title) + tool_id = f'spac_{tool_name}' + + # Get outputs from blueprint + outputs_spec = blueprint.get('outputs', {}) + if not outputs_spec: + outputs_spec = {'analysis': 'transform_output.pickle'} + + # Get template filename (could be in blueprint too) + template_filename = blueprint.get('templateFilename', + self.get_template_filename(clean_title, tool_name)) + + # Build sections - pass tool_name and outputs_spec for context + inputs_lines, multiple_file_inputs = self.build_inputs_section(blueprint, tool_name) + outputs_lines = self.build_outputs_section(outputs_spec) + command_section = self.build_command_section(tool_name, blueprint, multiple_file_inputs, outputs_spec) + + # Generate description + full_desc = self.clean_description(blueprint.get('description', '')) + short_desc = full_desc.split('\n')[0] if full_desc else '' + if len(short_desc) > 100: + short_desc = short_desc[:97] + '...' + + # Build help section + help_sections = [] + help_sections.append(f'**{title}**\n') + help_sections.append(f'{full_desc}\n') + help_sections.append('This tool is part of the SPAC (SPAtial single-Cell analysis) toolkit.\n') + + # Add usage notes based on input types + if blueprint.get('columns'): + help_sections.append('**Column Parameters:** Enter column names as text. Use comma-separation or one per line for multiple columns.') + + if any(p.get('paramType') == 'LIST' for p in blueprint.get('parameters', [])): + help_sections.append('**List Parameters:** Use comma-separated values or one per line.') + help_sections.append('**Special Values:** Enter "All" to select all items.') + + if multiple_file_inputs: + help_sections.append(f'**Multiple File Inputs:** This tool accepts multiple files for: {", ".join(multiple_file_inputs)}') + + help_text = '\n'.join(help_sections) + + # Generate complete XML + xml_content = f''' + {self.escape_xml(short_desc, False)} + + + {self.docker_image} + + + + python3 + + +{command_section} + + + + + + +{chr(10).join(inputs_lines)} + + + +{chr(10).join(outputs_lines)} + + + + + + +@misc{{spac_toolkit, + author = {{FNLCR DMAP Team}}, + title = {{SPAC: SPAtial single-Cell analysis}}, + year = {{2024}}, + url = {{https://github.com/FNLCR-DMAP/SCSAWorkflow}} +}} + + +''' + + # Write files + tool_dir = output_dir / tool_id + tool_dir.mkdir(parents=True, exist_ok=True) + + xml_path = tool_dir / f'{tool_id}.xml' + with open(xml_path, 'w') as f: + f.write(xml_content) + + # Copy wrapper script + if self.wrapper_script.exists(): + shutil.copy2(self.wrapper_script, tool_dir / 'run_spac_template.sh') + + # Copy runner script + if self.runner_script.exists(): + shutil.copy2(self.runner_script, tool_dir / 'spac_galaxy_runner.py') + else: + print(f" Warning: spac_galaxy_runner.py not found in current directory") + + return { + 'tool_id': tool_id, + 'tool_name': title, + 'xml_path': xml_path, + 'tool_dir': tool_dir, + 'template': template_filename, + 'outputs': outputs_spec + } + +def main(): + parser = argparse.ArgumentParser( + description='Convert NIDAP templates to Galaxy tools - Generalized Version' + ) + parser.add_argument('json_input', help='JSON file or directory') + parser.add_argument('-o', '--output-dir', default='galaxy_tools') + parser.add_argument('--docker-image', default='nciccbr/spac:v1') + + args = parser.parse_args() + + synthesizer = GeneralizedNIDAPToGalaxySynthesizer( + docker_image=args.docker_image + ) + + json_input = Path(args.json_input) + if json_input.is_file(): + json_files = [json_input] + elif json_input.is_dir(): + json_files = sorted(json_input.glob('*.json')) + else: + print(f"Error: {json_input} not found") + return 1 + + print(f"Processing {len(json_files)} files") + print(f"Docker image: {args.docker_image}") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + successful = [] + failed = [] + + for json_file in json_files: + print(f"\nProcessing: {json_file.name}") + try: + result = synthesizer.generate_tool(json_file, output_dir) + successful.append(result) + print(f" ✔ Created: {result['tool_id']}") + print(f" Template: {result['template']}") + print(f" Outputs: {list(result['outputs'].keys())}") + except Exception as e: + failed.append(json_file.name) + print(f" ✗ Failed: {e}") + import traceback + traceback.print_exc() + + print(f"\n{'='*60}") + print(f"Summary: {len(successful)} successful, {len(failed)} failed") + + if successful: + snippet_path = output_dir / 'tool_conf_snippet.xml' + with open(snippet_path, 'w') as f: + f.write('
\n') + for result in sorted(successful, key=lambda x: x['tool_id']): + tool_id = result['tool_id'] + f.write(f' \n') + f.write('
\n') + + print(f"\nGenerated tool configuration snippet: {snippet_path}") + + return 0 if not failed else 1 + +if __name__ == '__main__': + exit(main()) \ No newline at end of file diff --git a/galaxy_tools/refactor_tools/run_spac_template.sh b/galaxy_tools/refactor_tools/run_spac_template.sh new file mode 100644 index 00000000..3f2a7a3e --- /dev/null +++ b/galaxy_tools/refactor_tools/run_spac_template.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# run_spac_template.sh - Universal wrapper for SPAC Galaxy tools +set -eu + +PARAMS_JSON="${1:?Missing params.json path}" +TEMPLATE_NAME="${2:?Missing template name}" + +# Get the directory where this script is located (the tool directory) +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# Look for spac_galaxy_runner.py in multiple locations +if [ -f "$SCRIPT_DIR/spac_galaxy_runner.py" ]; then + # If it's in the same directory as this script + RUNNER_PATH="$SCRIPT_DIR/spac_galaxy_runner.py" +elif [ -f "$__tool_directory__/spac_galaxy_runner.py" ]; then + # If Galaxy provides tool directory + RUNNER_PATH="$__tool_directory__/spac_galaxy_runner.py" +else + # Fallback to trying the module approach + echo "Warning: spac_galaxy_runner.py not found locally, trying as module" >&2 + python3 -m spac_galaxy_runner "$PARAMS_JSON" "$TEMPLATE_NAME" + exit $? +fi + +# Run the runner script directly +echo "Running: python3 $RUNNER_PATH $PARAMS_JSON $TEMPLATE_NAME" >&2 +python3 "$RUNNER_PATH" "$PARAMS_JSON" "$TEMPLATE_NAME" \ No newline at end of file diff --git a/galaxy_tools/refactor_tools/spac_arcsinh_normalization.xml b/galaxy_tools/refactor_tools/spac_arcsinh_normalization.xml new file mode 100644 index 00000000..69a183b7 --- /dev/null +++ b/galaxy_tools/refactor_tools/spac_arcsinh_normalization.xml @@ -0,0 +1,69 @@ + + Normalize features either by a user-defined co-factor or a determined percentile, allowing for ef... + + + nciccbr/spac:v1 + + + + python3 + + + &2 && + cat "$params_json" >&2 && + echo "==================" >&2 && + + ## Save snapshot + cp "$params_json" params_snapshot.json && + + ## Run wrapper + bash $__tool_directory__/run_spac_template.sh "$params_json" "arcsinh_normalization" + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + +@misc{spac_toolkit, + author = {FNLCR DMAP Team}, + title = {SPAC: SPAtial single-Cell analysis}, + year = {2024}, + url = {https://github.com/FNLCR-DMAP/SCSAWorkflow} +} + + + \ No newline at end of file diff --git a/galaxy_tools/refactor_tools/spac_boxplot.xml b/galaxy_tools/refactor_tools/spac_boxplot.xml new file mode 100644 index 00000000..97c9ef88 --- /dev/null +++ b/galaxy_tools/refactor_tools/spac_boxplot.xml @@ -0,0 +1,85 @@ + + Create a boxplot visualization of the features in the analysis dataset. + + + nciccbr/spac:v1 + + + + python3 + + + &2 && + cat "$params_json" >&2 && + echo "==================" >&2 && + + ## Save snapshot + cp "$params_json" params_snapshot.json && + + ## Run wrapper + bash $__tool_directory__/run_spac_template.sh "$params_json" "boxplot" + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@misc{spac_toolkit, + author = {FNLCR DMAP Team}, + title = {SPAC: SPAtial single-Cell analysis}, + year = {2024}, + url = {https://github.com/FNLCR-DMAP/SCSAWorkflow} +} + + + \ No newline at end of file diff --git a/galaxy_tools/refactor_tools/spac_galaxy_runner.py b/galaxy_tools/refactor_tools/spac_galaxy_runner.py new file mode 100644 index 00000000..d8535936 --- /dev/null +++ b/galaxy_tools/refactor_tools/spac_galaxy_runner.py @@ -0,0 +1,515 @@ +#!/usr/bin/env python3 +""" +spac_galaxy_runner.py - Hybrid version combining refactored structure with robust parameter handling +Incorporates critical fixes from original wrapper for parameter processing +""" + +import json +import os +import sys +import subprocess +import shutil +from pathlib import Path +import re + +def main(): + """Main entry point for SPAC Galaxy runner""" + if len(sys.argv) != 3: + print("Usage: spac_galaxy_runner.py ") + sys.exit(1) + + params_path = sys.argv[1] + template_name = sys.argv[2] + + print(f"=== SPAC Galaxy Runner v2.0 (Hybrid) ===") + print(f"Template: {template_name}") + print(f"Parameters: {params_path}") + + # Load parameters + with open(params_path) as f: + params = json.load(f) + + # Extract outputs specification from environment variable + outputs_spec_env = os.environ.get('GALAXY_OUTPUTS_SPEC', '') + if outputs_spec_env: + try: + outputs = json.loads(outputs_spec_env) + except json.JSONDecodeError: + print(f"WARNING: Could not parse GALAXY_OUTPUTS_SPEC: {outputs_spec_env}") + outputs = determine_default_outputs(template_name) + else: + # Fallback: try to get from params + outputs = params.pop('outputs', {}) + if isinstance(outputs, str): + try: + outputs = json.loads(unsanitize_galaxy_params(outputs)) + except json.JSONDecodeError: + print(f"WARNING: Could not parse outputs: {outputs}") + outputs = determine_default_outputs(template_name) + + print(f"Outputs specification: {outputs}") + + # CRITICAL: Unsanitize and normalize parameters (from original) + params = process_galaxy_parameters(params, template_name) + + # Handle multiple file inputs that were copied to directories by Galaxy + handle_multiple_file_inputs(params) + + # Create output directories + create_output_directories(outputs) + + # Add output paths to params - critical for templates that save results + params['save_results'] = True + + if 'analysis' in outputs: + params['output_path'] = outputs['analysis'] + params['Output_Path'] = outputs['analysis'] + params['Output_File'] = outputs['analysis'] + + if 'DataFrames' in outputs: + df_path = outputs['DataFrames'] + # Check if it's a single file or a directory + if df_path.endswith('.csv') or df_path.endswith('.tsv'): + # Single file output (like Load CSV Files) + params['output_file'] = df_path + params['Output_File'] = df_path + print(f" Set output_file to: {df_path}") + else: + # Directory for multiple files (like boxplot) + params['output_dir'] = df_path + params['Export_Dir'] = df_path + params['Output_File'] = os.path.join(df_path, f'{template_name}_output.csv') + print(f" Set output_dir to: {df_path}") + + if 'figures' in outputs: + fig_dir = outputs['figures'] + params['figure_dir'] = fig_dir + params['Figure_Dir'] = fig_dir + params['Figure_File'] = os.path.join(fig_dir, f'{template_name}.png') + print(f" Set figure_dir to: {fig_dir}") + + if 'html' in outputs: + html_dir = outputs['html'] + params['html_dir'] = html_dir + params['Output_File'] = os.path.join(html_dir, f'{template_name}.html') + print(f" Set html_dir to: {html_dir}") + + # Save config for debugging (without outputs key) + with open('config_used.json', 'w') as f: + config_data = {k: v for k, v in params.items() if k not in ['outputs']} + json.dump(config_data, f, indent=2) + + # Save params for template execution + with open('params_exec.json', 'w') as f: + json.dump(params, f, indent=2) + + # Find and execute template + template_path = find_template(template_name) + if not template_path: + print(f"ERROR: Template for {template_name} not found") + sys.exit(1) + + # Run template + exit_code = execute_template(template_path, 'params_exec.json') + if exit_code != 0: + print(f"ERROR: Template failed with exit code {exit_code}") + sys.exit(exit_code) + + # Handle output mapping for specific tools + handle_output_mapping(template_name, outputs) + + # Verify outputs + verify_outputs(outputs) + + # Save snapshot for debugging + with open('params_snapshot.json', 'w') as f: + json.dump(params, f, indent=2) + + print("=== Execution Complete ===") + sys.exit(0) + +def unsanitize_galaxy_params(s: str) -> str: + """Remove Galaxy's parameter sanitization tokens""" + if not isinstance(s, str): + return s + replacements = { + '__ob__': '[', '__cb__': ']', + '__oc__': '{', '__cc__': '}', + '__dq__': '"', '__sq__': "'", + '__gt__': '>', '__lt__': '<', + '__cn__': '\n', '__cr__': '\r', + '__tc__': '\t', '__pd__': '#', + '__at__': '@', '__cm__': ',' + } + for token, char in replacements.items(): + s = s.replace(token, char) + return s + +def process_galaxy_parameters(params: dict, template_name: str) -> dict: + """Process Galaxy parameters - unsanitize and normalize (from original wrapper)""" + print("\n=== Processing Galaxy Parameters ===") + + # Step 1: Recursively unsanitize all parameters + def recursive_unsanitize(obj): + if isinstance(obj, str): + unsanitized = unsanitize_galaxy_params(obj).strip() + # Try to parse JSON strings + if (unsanitized.startswith('[') and unsanitized.endswith(']')) or \ + (unsanitized.startswith('{') and unsanitized.endswith('}')): + try: + return json.loads(unsanitized) + except: + return unsanitized + return unsanitized + elif isinstance(obj, dict): + return {k: recursive_unsanitize(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [recursive_unsanitize(item) for item in obj] + return obj + + params = recursive_unsanitize(params) + + # Step 2: Handle specific parameter normalizations + + # Special handling for String_Columns in load_csv templates + if 'load_csv' in template_name and 'String_Columns' in params: + value = params['String_Columns'] + if not isinstance(value, list): + if value in [None, "", "[]", "__ob____cb__", []]: + params['String_Columns'] = [] + elif isinstance(value, str): + s = value.strip() + if s and s != '[]': + if ',' in s: + params['String_Columns'] = [item.strip() for item in s.split(',') if item.strip()] + else: + params['String_Columns'] = [s] if s else [] + else: + params['String_Columns'] = [] + else: + params['String_Columns'] = [] + print(f" Normalized String_Columns: {params['String_Columns']}") + + # Handle Feature_Regex specially - MUST BE AFTER Features_to_Analyze processing + if 'Feature_Regex' in params: + value = params['Feature_Regex'] + if value in [[], [""], "__ob____cb__", "[]", "", None]: + params['Feature_Regex'] = [] + print(" Cleared empty Feature_Regex parameter") + elif isinstance(value, list) and value: + # Join regex patterns with | + params['Feature_Regex'] = "|".join(str(v) for v in value if v) + print(f" Joined Feature_Regex list: {params['Feature_Regex']}") + + # Handle Features_to_Analyze - split if it's a single string with spaces or commas + if 'Features_to_Analyze' in params: + value = params['Features_to_Analyze'] + if isinstance(value, str): + # Check for comma-separated or space-separated features + if ',' in value: + params['Features_to_Analyze'] = [item.strip() for item in value.split(',') if item.strip()] + print(f" Split Features_to_Analyze on comma: {value} -> {params['Features_to_Analyze']}") + elif ' ' in value: + # This is likely multiple features in a single string + params['Features_to_Analyze'] = [item.strip() for item in value.split() if item.strip()] + print(f" Split Features_to_Analyze on space: {value} -> {params['Features_to_Analyze']}") + elif value: + params['Features_to_Analyze'] = [value] + print(f" Wrapped Features_to_Analyze in list: {params['Features_to_Analyze']}") + + # Handle Feature_s_to_Plot for boxplot + if 'Feature_s_to_Plot' in params: + value = params['Feature_s_to_Plot'] + # Check if it's "All" + if value == "All" or value == ["All"]: + params['Feature_s_to_Plot'] = ["All"] + print(" Set Feature_s_to_Plot to ['All']") + elif isinstance(value, str) and value not in ["", "[]"]: + params['Feature_s_to_Plot'] = [value] + print(f" Wrapped Feature_s_to_Plot in list: {params['Feature_s_to_Plot']}") + + # Normalize list parameters + list_params = ['Annotation_s_', 'Features', 'Markers', 'Markers_to_Plot', + 'Phenotypes', 'Binary_Phenotypes', 'Features_to_Analyze'] + + for key in list_params: + if key in params: + value = params[key] + if not isinstance(value, list): + if value in [None, ""]: + continue + elif isinstance(value, str): + if ',' in value: + params[key] = [item.strip() for item in value.split(',') if item.strip()] + print(f" Split {key} on comma: {params[key]}") + else: + params[key] = [value] + print(f" Wrapped {key} in list: {params[key]}") + + # Fix single-element lists for coordinate columns + coordinate_keys = ['X_Coordinate_Column', 'Y_Coordinate_Column', + 'X_centroid', 'Y_centroid', 'Primary_Annotation', + 'Secondary_Annotation', 'Annotation'] + + for key in coordinate_keys: + if key in params: + value = params[key] + if isinstance(value, list) and len(value) == 1: + params[key] = value[0] + print(f" Extracted single value from {key}: {params[key]}") + + return params + +def determine_default_outputs(template_name: str) -> dict: + """Determine default outputs based on template name""" + if 'boxplot' in template_name or 'plot' in template_name or 'histogram' in template_name: + return {'DataFrames': 'dataframe_folder', 'figures': 'figure_folder'} + elif 'load_csv' in template_name: + # Load CSV Files produces a single CSV file, not a folder + return {'DataFrames': 'combined_data.csv'} + elif 'interactive' in template_name: + return {'html': 'html_folder'} + else: + return {'analysis': 'transform_output.pickle'} + +def handle_multiple_file_inputs(params): + """ + Handle multiple file inputs that Galaxy copies to directories. + Galaxy copies multiple files to xxx_dir directories. + """ + print("\n=== Handling Multiple File Inputs ===") + + # Check for directory inputs that indicate multiple files + for key in list(params.keys()): + # Check if Galaxy created a _dir directory for this input + dir_name = f"{key}_dir" + if os.path.isdir(dir_name): + params[key] = dir_name + print(f" Updated {key} -> {dir_name}") + # List files in the directory + files = os.listdir(dir_name) + print(f" Contains {len(files)} files") + for f in files[:3]: + print(f" - {f}") + if len(files) > 3: + print(f" ... and {len(files)-3} more") + + # Special case for CSV_Files (Load CSV Files tool) + if 'CSV_Files' in params: + # Check for csv_input_dir created by Galaxy command + if os.path.exists('csv_input_dir') and os.path.isdir('csv_input_dir'): + params['CSV_Files'] = 'csv_input_dir' + print(f" Using csv_input_dir for CSV_Files") + elif os.path.isdir('CSV_Files_dir'): + params['CSV_Files'] = 'CSV_Files_dir' + print(f" Updated CSV_Files -> CSV_Files_dir") + elif isinstance(params['CSV_Files'], str) and os.path.isfile(params['CSV_Files']): + # Single file - get its directory + params['CSV_Files'] = os.path.dirname(params['CSV_Files']) + print(f" Using directory of CSV file: {params['CSV_Files']}") + +def create_output_directories(outputs): + """Create directories for collection outputs""" + print("\n=== Creating Output Directories ===") + + for output_type, path in outputs.items(): + if path.endswith('_folder') or path.endswith('_dir'): + # This is a directory for multiple files + os.makedirs(path, exist_ok=True) + print(f" Created directory: {path}") + else: + # For single files, ensure parent directory exists if there is one + parent = os.path.dirname(path) + if parent and not os.path.exists(parent): + os.makedirs(parent, exist_ok=True) + print(f" Created parent directory: {parent}") + else: + print(f" Single file output: {path} (no directory needed)") + + # Add output parameters to params for templates that need them + # This is critical for templates like boxplot that check for these + return outputs + +def find_template(template_name): + """Find the template Python file""" + print("\n=== Finding Template ===") + + # Determine template filename + if template_name == 'load_csv_files': + template_py = 'load_csv_files_with_config.py' + else: + template_py = f'{template_name}_template.py' + + # Search paths (adjust based on your container/environment) + search_paths = [ + f'/opt/spac/templates/{template_py}', + f'/app/spac/templates/{template_py}', + f'/opt/SCSAWorkflow/src/spac/templates/{template_py}', + f'/usr/local/lib/python3.9/site-packages/spac/templates/{template_py}', + f'./templates/{template_py}', + f'./{template_py}' + ] + + for path in search_paths: + if os.path.exists(path): + print(f" Found: {path}") + return path + + print(f" ERROR: {template_py} not found in:") + for path in search_paths: + print(f" - {path}") + return None + +def execute_template(template_path, params_file): + """Execute the SPAC template""" + print("\n=== Executing Template ===") + print(f" Command: python3 {template_path} {params_file}") + + # Run template and capture output + result = subprocess.run( + ['python3', template_path, params_file], + capture_output=True, + text=True + ) + + # Save stdout and stderr + with open('tool_stdout.txt', 'w') as f: + f.write("=== STDOUT ===\n") + f.write(result.stdout) + if result.stderr: + f.write("\n=== STDERR ===\n") + f.write(result.stderr) + + # Display output + if result.stdout: + print(" Output:") + lines = result.stdout.split('\n') + for line in lines[:20]: # First 20 lines + print(f" {line}") + if len(lines) > 20: + print(f" ... ({len(lines)-20} more lines)") + + if result.stderr: + print(" Errors:", file=sys.stderr) + for line in result.stderr.split('\n'): + if line.strip(): + print(f" {line}", file=sys.stderr) + + return result.returncode + +def handle_output_mapping(template_name, outputs): + """ + Map template outputs to expected locations. + Generic approach: find outputs based on pattern matching. + """ + print("\n=== Output Mapping ===") + + for output_type, expected_path in outputs.items(): + # Skip if already exists at expected location + if os.path.exists(expected_path): + print(f" {output_type}: Already at {expected_path}") + continue + + # Handle single file outputs + if expected_path.endswith('.csv') or expected_path.endswith('.tsv') or \ + expected_path.endswith('.pickle') or expected_path.endswith('.h5ad'): + find_and_move_output(output_type, expected_path) + + # Handle folder outputs - check if a default folder exists + elif expected_path.endswith('_folder') or expected_path.endswith('_dir'): + default_folder = output_type.lower() + '_folder' + if default_folder != expected_path and os.path.isdir(default_folder): + print(f" Moving {default_folder} to {expected_path}") + shutil.move(default_folder, expected_path) + +def find_and_move_output(output_type, expected_path): + """ + Find output file based on extension and move to expected location. + More generic approach without hardcoded paths. + """ + ext = os.path.splitext(expected_path)[1] # e.g., '.csv' + basename = os.path.basename(expected_path) + + print(f" Looking for {output_type} output ({ext} file)...") + + # Search in common output locations + search_dirs = ['.', 'dataframe_folder', 'output', 'results'] + + for search_dir in search_dirs: + if not os.path.exists(search_dir): + continue + + if os.path.isdir(search_dir): + # Find files with matching extension + matches = [f for f in os.listdir(search_dir) + if f.endswith(ext)] + + if len(matches) == 1: + source = os.path.join(search_dir, matches[0]) + print(f" Found: {source}") + print(f" Moving to: {expected_path}") + shutil.move(source, expected_path) + return + elif len(matches) > 1: + # Multiple matches - use the largest or most recent + matches_with_size = [(f, os.path.getsize(os.path.join(search_dir, f))) + for f in matches] + matches_with_size.sort(key=lambda x: x[1], reverse=True) + source = os.path.join(search_dir, matches_with_size[0][0]) + print(f" Found multiple {ext} files, using largest: {source}") + shutil.move(source, expected_path) + return + + # Also check if file exists with different name in current dir + current_dir_matches = [f for f in os.listdir('.') + if f.endswith(ext) and f != basename] + if current_dir_matches: + source = current_dir_matches[0] + print(f" Found: {source}") + print(f" Moving to: {expected_path}") + shutil.move(source, expected_path) + return + + print(f" WARNING: No {ext} file found for {output_type}") + +def verify_outputs(outputs): + """Verify that expected outputs were created""" + print("\n=== Output Verification ===") + + all_found = True + for output_type, path in outputs.items(): + if os.path.exists(path): + if os.path.isdir(path): + files = os.listdir(path) + total_size = sum(os.path.getsize(os.path.join(path, f)) + for f in files) + print(f" ✔ {output_type}: {len(files)} files in {path} " + f"({format_size(total_size)})") + # Show first few files + for f in files[:3]: + size = os.path.getsize(os.path.join(path, f)) + print(f" - {f} ({format_size(size)})") + if len(files) > 3: + print(f" ... and {len(files)-3} more") + else: + size = os.path.getsize(path) + print(f" ✔ {output_type}: {path} ({format_size(size)})") + else: + print(f" ✗ {output_type}: NOT FOUND at {path}") + all_found = False + + if not all_found: + print("\n WARNING: Some outputs not found!") + print(" Check tool_stdout.txt for errors") + # Don't exit with error - let Galaxy handle missing outputs + +def format_size(bytes): + """Format byte size in human-readable format""" + for unit in ['B', 'KB', 'MB', 'GB']: + if bytes < 1024.0: + return f"{bytes:.1f} {unit}" + bytes /= 1024.0 + return f"{bytes:.1f} TB" + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/galaxy_tools/refactor_tools/spac_load_csv_files.xml b/galaxy_tools/refactor_tools/spac_load_csv_files.xml new file mode 100644 index 00000000..5d71d104 --- /dev/null +++ b/galaxy_tools/refactor_tools/spac_load_csv_files.xml @@ -0,0 +1,75 @@ + + Load CSV files from NIDAP dataset and combine them into a single pandas dataframe for downstream ... + + + nciccbr/spac:v1 + + + + python3 + + + &2 && + cat "$params_json" >&2 && + echo "==================" >&2 && + + ## Save snapshot + cp "$params_json" params_snapshot.json && + + ## Run wrapper + bash $__tool_directory__/run_spac_template.sh "$params_json" "load_csv_files" + ]]> + + + + + + + + + + + + + + + + + + + + + + +@misc{spac_toolkit, + author = {FNLCR DMAP Team}, + title = {SPAC: SPAtial single-Cell analysis}, + year = {2024}, + url = {https://github.com/FNLCR-DMAP/SCSAWorkflow} +} + + + \ No newline at end of file diff --git a/galaxy_tools/refactor_tools/spac_setup_analysis.xml b/galaxy_tools/refactor_tools/spac_setup_analysis.xml new file mode 100644 index 00000000..f762f78d --- /dev/null +++ b/galaxy_tools/refactor_tools/spac_setup_analysis.xml @@ -0,0 +1,71 @@ + + Convert the pre-processed dataset to the analysis object for downstream analysis. + + + nciccbr/spac:v1 + + + + python3 + + + &2 && + cat "$params_json" >&2 && + echo "==================" >&2 && + + ## Save snapshot + cp "$params_json" params_snapshot.json && + + ## Run wrapper + bash $__tool_directory__/run_spac_template.sh "$params_json" "setup_analysis" + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + +@misc{spac_toolkit, + author = {FNLCR DMAP Team}, + title = {SPAC: SPAtial single-Cell analysis}, + year = {2024}, + url = {https://github.com/FNLCR-DMAP/SCSAWorkflow} +} + + + \ No newline at end of file From 4906439dfdbec132ba675e4d13b9c48cd33d8c38 Mon Sep 17 00:00:00 2001 From: abmahmoud03 Date: Sun, 19 Oct 2025 02:49:46 -0400 Subject: [PATCH 091/102] fix(boxplot): replace deprecated append call with concat --- src/spac/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/spac/utils.py b/src/spac/utils.py index f3506a20..ff8b8147 100644 --- a/src/spac/utils.py +++ b/src/spac/utils.py @@ -1108,12 +1108,13 @@ def compute_metrics(data): # Ensure the maximum and minimum outliers are included max_outlier = outlier_series.max() min_outlier = outlier_series.min() - outliers_sampled = outliers_sampled.append( - pd.Series([max_outlier, min_outlier]) + outliers_sampled = pd.concat( + [outliers_sampled, pd.Series([max_outlier, min_outlier])], + ignore_index=True ) # Convert the sampled values back to a list - outliers = outliers_sampled.reset_index(drop=True).tolist() + outliers = outliers_sampled.tolist() metrics = [ lower_whisker, From 862e523d08f40bb1e0aee53437fa06bdb533ac45 Mon Sep 17 00:00:00 2001 From: abmahmoud03 Date: Sun, 19 Oct 2025 02:52:33 -0400 Subject: [PATCH 092/102] feat(test_performance): add performance tests for boxplot/histogram --- tests/test_performance/__init__.py | 1 + .../test_boxplot_performance.py | 203 ++++++++++ .../test_histogram_performance.py | 372 ++++++++++++++++++ 3 files changed, 576 insertions(+) create mode 100644 tests/test_performance/__init__.py create mode 100644 tests/test_performance/test_boxplot_performance.py create mode 100644 tests/test_performance/test_histogram_performance.py diff --git a/tests/test_performance/__init__.py b/tests/test_performance/__init__.py new file mode 100644 index 00000000..f4b89e27 --- /dev/null +++ b/tests/test_performance/__init__.py @@ -0,0 +1 @@ +"""Performance tests for SPAC visualization functions.""" diff --git a/tests/test_performance/test_boxplot_performance.py b/tests/test_performance/test_boxplot_performance.py new file mode 100644 index 00000000..db6754dc --- /dev/null +++ b/tests/test_performance/test_boxplot_performance.py @@ -0,0 +1,203 @@ +import unittest +import time +import numpy as np +import pandas as pd +import anndata as ad +import matplotlib +import matplotlib.pyplot as plt +from sklearn.datasets import make_blobs +from sklearn.preprocessing import StandardScaler +from spac.visualization import boxplot, boxplot_interactive + +matplotlib.use('Agg') # Set the backend to 'Agg' to suppress plot window + + +class TestBoxplotPerformance(unittest.TestCase): + """Performance comparison tests for boxplot vs boxplot_interactive.""" + + @classmethod + def setUpClass(cls): + """Generate large datasets once for all tests.""" + print("\n" + "=" * 70) + print("Setting up large datasets for boxplot performance tests...") + print("=" * 70) + + # Generate 1M cell dataset + print("\nGenerating 1M cell dataset...") + start = time.time() + cls.adata_1m = cls._generate_dataset(n_obs=1_000_000, random_state=42) + print(f" Completed in {time.time() - start:.2f} seconds") + + # Generate 5M cell dataset + print("\nGenerating 5M cell dataset...") + start = time.time() + cls.adata_5m = cls._generate_dataset(n_obs=5_000_000, random_state=42) + print(f" Completed in {time.time() - start:.2f} seconds") + + # Generate 10M cell dataset + print("\nGenerating 10M cell dataset...") + start = time.time() + cls.adata_10m = cls._generate_dataset(n_obs=10_000_000, random_state=42) + print(f" Completed in {time.time() - start:.2f} seconds") + print("=" * 70 + "\n") + + @staticmethod + def _generate_dataset(n_obs: int, random_state: int = 42) -> ad.AnnData: + """ + Generate a synthetic AnnData object with realistic clustering. + + Creates dataset with: + - 5 features (marker_1 to marker_5) + - 5 annotations (cell_type, phenotype, region, batch, treatment) + - 3 layers (normalized, log_transformed, scaled) + """ + np.random.seed(random_state) + + # Generate base data with natural clustering + n_features = 5 + n_centers = 5 + + X, cluster_labels = make_blobs( + n_samples=n_obs, + n_features=n_features, + centers=n_centers, + cluster_std=1.5, + random_state=random_state + ) + + # Make values positive and add variation + X = np.abs(X) + np.random.exponential(scale=2.0, size=X.shape) + + # Create feature names + feature_names = [f"marker_{i+1}" for i in range(n_features)] + + # Create annotations based on clusters + cell_types = [f"Type_{chr(65+i)}" for i in range(5)] + cell_type = np.array([cell_types[i % 5] for i in cluster_labels]) + + phenotypes = [f"Pheno_{i+1}" for i in range(4)] + phenotype = np.array([phenotypes[i % 4] for i in cluster_labels]) + random_mask = np.random.random(n_obs) < 0.2 + phenotype[random_mask] = np.random.choice(phenotypes, size=random_mask.sum()) + + regions = ["Region_X", "Region_Y", "Region_Z"] + region = np.random.choice(regions, size=n_obs) + + batches = ["Batch_1", "Batch_2", "Batch_3"] + batch = np.random.choice(batches, size=n_obs) + + treatments = ["Control", "Treated"] + treatment = np.random.choice(treatments, size=n_obs, p=[0.5, 0.5]) + + # Create observations DataFrame + obs = pd.DataFrame({ + 'cell_type': pd.Categorical(cell_type), + 'phenotype': pd.Categorical(phenotype), + 'region': pd.Categorical(region), + 'batch': pd.Categorical(batch), + 'treatment': pd.Categorical(treatment) + }) + + # Create AnnData object + adata = ad.AnnData(X=X, obs=obs) + adata.var_names = feature_names + + # Create layers with different transformations + X_normalized = np.zeros_like(X) + for i in range(n_features): + feature_min = X[:, i].min() + feature_max = X[:, i].max() + X_normalized[:, i] = (X[:, i] - feature_min) / (feature_max - feature_min) + adata.layers['normalized'] = X_normalized + + adata.layers['log_transformed'] = np.log1p(X) + + scaler = StandardScaler() + adata.layers['scaled'] = scaler.fit_transform(X) + + return adata + + def tearDown(self): + """Clean up matplotlib figures after each test.""" + plt.close('all') + + def _run_comparison(self, adata, test_name): + """Run comparison between boxplot and boxplot_interactive.""" + n_obs = adata.n_obs + features = ['marker_1', 'marker_2', 'marker_3', 'marker_4', 'marker_5'] + annotation = 'cell_type' + layer = 'normalized' + + print(f"\n{'=' * 70}") + print(f"{test_name}: {n_obs:,} cells") + print(f" Features: {', '.join(features)}") + print(f" Annotation: {annotation}") + print(f" Layer: {layer}") + print(f"{'=' * 70}") + + # Test boxplot + print("\n Running boxplot...") + start = time.time() + fig, ax, df = boxplot( + adata, + features=features, + annotation=annotation, + layer=layer + ) + boxplot_time = time.time() - start + print(f" Time: {boxplot_time:.2f} seconds") + plt.close('all') + + # Test boxplot_interactive with downsampling + print("\n Running boxplot_interactive (with downsampling)...") + start = time.time() + result = boxplot_interactive( + adata, + features=features, + annotation=annotation, + layer=layer, + showfliers='downsample' + ) + interactive_time = time.time() - start + print(f" Time: {interactive_time:.2f} seconds") + + # Calculate speedup + speedup = boxplot_time / interactive_time if interactive_time > 0 else 0 + + print(f"\n Results:") + print(f" boxplot: {boxplot_time:.2f}s") + print(f" boxplot_interactive: {interactive_time:.2f}s") + print(f" Speedup factor: {speedup:.2f}x") + + if speedup > 1: + print(f" → boxplot is {speedup:.2f}x faster") + elif speedup < 1: + print(f" → boxplot_interactive is {1/speedup:.2f}x faster") + else: + print(f" → Both functions have similar performance") + + print(f"{'=' * 70}\n") + + # Store results for potential further analysis + return { + 'n_obs': n_obs, + 'boxplot_time': boxplot_time, + 'boxplot_interactive_time': interactive_time, + 'speedup_factor': speedup + } + + def test_comparison_1m(self): + """Compare boxplot vs boxplot_interactive with 1M cells.""" + self._run_comparison(self.adata_1m, "Boxplot Performance Comparison [1M cells]") + + def test_comparison_5m(self): + """Compare boxplot vs boxplot_interactive with 5M cells.""" + self._run_comparison(self.adata_5m, "Boxplot Performance Comparison [5M cells]") + + def test_comparison_10m(self): + """Compare boxplot vs boxplot_interactive with 10M cells.""" + self._run_comparison(self.adata_10m, "Boxplot Performance Comparison [10M cells]") + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tests/test_performance/test_histogram_performance.py b/tests/test_performance/test_histogram_performance.py new file mode 100644 index 00000000..5cc6c14b --- /dev/null +++ b/tests/test_performance/test_histogram_performance.py @@ -0,0 +1,372 @@ +import unittest +import time +import warnings +import numpy as np +import pandas as pd +import anndata as ad +import matplotlib +import matplotlib.pyplot as plt +import seaborn as sns +from sklearn.datasets import make_blobs +from sklearn.preprocessing import StandardScaler +from spac.visualization import histogram +from spac.utils import check_annotation, check_feature, check_table + +matplotlib.use('Agg') # Set the backend to 'Agg' to suppress plot window + + +class TestHistogramPerformance(unittest.TestCase): + """Performance comparison tests for histogram vs histogram_old.""" + + @classmethod + def setUpClass(cls): + """Generate large datasets once for all tests.""" + print("\n" + "=" * 70) + print("Setting up large datasets for histogram performance tests...") + print("=" * 70) + + # Generate 1M cell dataset + print("\nGenerating 1M cell dataset...") + start = time.time() + cls.adata_1m = cls._generate_dataset(n_obs=1_000_000, random_state=42) + print(f" Completed in {time.time() - start:.2f} seconds") + + # Generate 5M cell dataset + print("\nGenerating 5M cell dataset...") + start = time.time() + cls.adata_5m = cls._generate_dataset(n_obs=5_000_000, random_state=42) + print(f" Completed in {time.time() - start:.2f} seconds") + + # Generate 10M cell dataset + print("\nGenerating 10M cell dataset...") + start = time.time() + cls.adata_10m = cls._generate_dataset(n_obs=10_000_000, random_state=42) + print(f" Completed in {time.time() - start:.2f} seconds") + print("=" * 70 + "\n") + + @staticmethod + def _generate_dataset(n_obs: int, random_state: int = 42) -> ad.AnnData: + """ + Generate a synthetic AnnData object with realistic clustering. + + Creates dataset with: + - 5 features (marker_1 to marker_5) + - 5 annotations (cell_type, phenotype, region, batch, treatment) + - 3 layers (normalized, log_transformed, scaled) + """ + np.random.seed(random_state) + + # Generate base data with natural clustering + n_features = 5 + n_centers = 5 + + X, cluster_labels = make_blobs( + n_samples=n_obs, + n_features=n_features, + centers=n_centers, + cluster_std=1.5, + random_state=random_state + ) + + # Make values positive and add variation + X = np.abs(X) + np.random.exponential(scale=2.0, size=X.shape) + + # Create feature names + feature_names = [f"marker_{i+1}" for i in range(n_features)] + + # Create annotations based on clusters + cell_types = [f"Type_{chr(65+i)}" for i in range(5)] + cell_type = np.array([cell_types[i % 5] for i in cluster_labels]) + + phenotypes = [f"Pheno_{i+1}" for i in range(4)] + phenotype = np.array([phenotypes[i % 4] for i in cluster_labels]) + random_mask = np.random.random(n_obs) < 0.2 + phenotype[random_mask] = np.random.choice(phenotypes, size=random_mask.sum()) + + regions = ["Region_X", "Region_Y", "Region_Z"] + region = np.random.choice(regions, size=n_obs) + + batches = ["Batch_1", "Batch_2", "Batch_3"] + batch = np.random.choice(batches, size=n_obs) + + treatments = ["Control", "Treated"] + treatment = np.random.choice(treatments, size=n_obs, p=[0.5, 0.5]) + + # Create observations DataFrame + obs = pd.DataFrame({ + 'cell_type': pd.Categorical(cell_type), + 'phenotype': pd.Categorical(phenotype), + 'region': pd.Categorical(region), + 'batch': pd.Categorical(batch), + 'treatment': pd.Categorical(treatment) + }) + + # Create AnnData object + adata = ad.AnnData(X=X, obs=obs) + adata.var_names = feature_names + + # Create layers with different transformations + X_normalized = np.zeros_like(X) + for i in range(n_features): + feature_min = X[:, i].min() + feature_max = X[:, i].max() + X_normalized[:, i] = (X[:, i] - feature_min) / (feature_max - feature_min) + adata.layers['normalized'] = X_normalized + + adata.layers['log_transformed'] = np.log1p(X) + + scaler = StandardScaler() + adata.layers['scaled'] = scaler.fit_transform(X) + + return adata + + def tearDown(self): + """Clean up matplotlib figures after each test.""" + plt.close('all') + + @staticmethod + def histogram_old(adata, feature=None, annotation=None, layer=None, + group_by=None, together=False, ax=None, + x_log_scale=False, y_log_scale=False, **kwargs): + """Old histogram implementation for performance comparison.""" + # If no feature or annotation is specified, apply default behavior + if feature is None and annotation is None: + feature = adata.var_names[0] + warnings.warn( + "No feature or annotation specified. " + "Defaulting to the first feature: " + f"'{feature}'.", + UserWarning + ) + + # Use utility functions for input validation + if layer: + check_table(adata, tables=layer) + if annotation: + check_annotation(adata, annotations=annotation) + if feature: + check_feature(adata, features=feature) + if group_by: + check_annotation(adata, annotations=group_by) + + # If layer is specified, get the data from that layer + if layer: + df = pd.DataFrame( + adata.layers[layer], index=adata.obs.index, columns=adata.var_names + ) + else: + df = pd.DataFrame( + adata.X, index=adata.obs.index, columns=adata.var_names + ) + layer = 'Original' + + df = pd.concat([df, adata.obs], axis=1) + + if feature and annotation: + raise ValueError("Cannot pass both feature and annotation," + " choose one.") + + data_column = feature if feature else annotation + + # Check for negative values and apply log1p transformation if x_log_scale is True + if x_log_scale: + if (df[data_column] < 0).any(): + print( + "There are negative values in the data, disabling x_log_scale." + ) + x_log_scale = False + else: + df[data_column] = np.log1p(df[data_column]) + + if ax is not None: + fig = ax.get_figure() + else: + fig, ax = plt.subplots() + + axs = [] + + # Prepare the data for plotting + plot_data = df.dropna(subset=[data_column]) + + # Bin calculation section + def cal_bin_num(num_rows): + bins = max(int(2*(num_rows ** (1/3))), 1) + print(f'Automatically calculated number of bins is: {bins}') + return(bins) + + num_rows = plot_data.shape[0] + + # Check if bins is being passed + if 'bins' not in kwargs: + kwargs['bins'] = cal_bin_num(num_rows) + + # Plotting with or without grouping + if group_by: + groups = df[group_by].dropna().unique().tolist() + n_groups = len(groups) + if n_groups == 0: + raise ValueError("There must be at least one group to create a" + " histogram.") + + if together: + kwargs.setdefault("multiple", "stack") + kwargs.setdefault("element", "bars") + + sns.histplot(data=df.dropna(), x=data_column, hue=group_by, + ax=ax, **kwargs) + if feature: + ax.set_title(f'Layer: {layer}') + axs.append(ax) + else: + fig, ax_array = plt.subplots( + n_groups, 1, figsize=(5, 5 * n_groups) + ) + + if n_groups == 1: + ax_array = [ax_array] + else: + ax_array = ax_array.flatten() + + for i, ax_i in enumerate(ax_array): + group_data = plot_data[plot_data[group_by] == groups[i]] + + sns.histplot(data=group_data, x=data_column, ax=ax_i, **kwargs) + if feature: + ax_i.set_title(f'{groups[i]} with Layer: {layer}') + else: + ax_i.set_title(f'{groups[i]}') + + if y_log_scale: + ax_i.set_yscale('log') + + if x_log_scale: + xlabel = f'log({data_column})' + else: + xlabel = data_column + ax_i.set_xlabel(xlabel) + + stat = kwargs.get('stat', 'count') + ylabel_map = { + 'count': 'Count', + 'frequency': 'Frequency', + 'density': 'Density', + 'probability': 'Probability' + } + ylabel = ylabel_map.get(stat, 'Count') + if y_log_scale: + ylabel = f'log({ylabel})' + ax_i.set_ylabel(ylabel) + + axs.append(ax_i) + else: + sns.histplot(data=plot_data, x=data_column, ax=ax, **kwargs) + if feature: + ax.set_title(f'Layer: {layer}') + axs.append(ax) + + if y_log_scale: + ax.set_yscale('log') + + if x_log_scale: + xlabel = f'log({data_column})' + else: + xlabel = data_column + ax.set_xlabel(xlabel) + + stat = kwargs.get('stat', 'count') + ylabel_map = { + 'count': 'Count', + 'frequency': 'Frequency', + 'density': 'Density', + 'probability': 'Probability' + } + ylabel = ylabel_map.get(stat, 'Count') + if y_log_scale: + ylabel = f'log({ylabel})' + ax.set_ylabel(ylabel) + + if len(axs) == 1: + return fig, axs[0] + else: + return fig, axs + + def _run_comparison(self, adata, test_name): + """Run comparison between histogram_old and histogram.""" + n_obs = adata.n_obs + feature = 'marker_1' + annotation = None + layer = 'normalized' + + print(f"\n{'=' * 70}") + print(f"{test_name}: {n_obs:,} cells") + print(f" Feature: {feature}") + print(f" Annotation: {annotation}") + print(f" Layer: {layer}") + print(f"{'=' * 70}") + + # Test histogram_old + print("\n Running histogram_old...") + start = time.time() + fig_old, ax_old = self.histogram_old( + adata, + feature=feature, + annotation=annotation, + layer=layer + ) + old_time = time.time() - start + print(f" Time: {old_time:.2f} seconds") + plt.close('all') + + # Test histogram from SPAC + print("\n Running histogram (SPAC)...") + start = time.time() + result = histogram( + adata, + feature=feature, + annotation=annotation, + layer=layer + ) + new_time = time.time() - start + print(f" Time: {new_time:.2f} seconds") + plt.close('all') + + # Calculate speedup + speedup = old_time / new_time if new_time > 0 else 0 + + print(f"\n Results:") + print(f" histogram_old: {old_time:.2f}s") + print(f" histogram: {new_time:.2f}s") + print(f" Speedup factor: {speedup:.2f}x") + + if speedup > 1: + print(f" → histogram (SPAC) is {speedup:.2f}x faster") + elif speedup < 1: + print(f" → histogram_old is {1/speedup:.2f}x faster") + else: + print(f" → Both functions have similar performance") + + print(f"{'=' * 70}\n") + + # Store results for potential further analysis + return { + 'n_obs': n_obs, + 'histogram_old_time': old_time, + 'histogram_time': new_time, + 'speedup_factor': speedup + } + + def test_comparison_1m(self): + """Compare histogram_old vs histogram with 1M cells.""" + self._run_comparison(self.adata_1m, "Histogram Performance Comparison [1M cells]") + + def test_comparison_5m(self): + """Compare histogram_old vs histogram with 5M cells.""" + self._run_comparison(self.adata_5m, "Histogram Performance Comparison [5M cells]") + + def test_comparison_10m(self): + """Compare histogram_old vs histogram with 10M cells.""" + self._run_comparison(self.adata_10m, "Histogram Performance Comparison [10M cells]") + + +if __name__ == '__main__': + unittest.main(verbosity=2) From c0762c34c453e35bbe07e9a57180686cde1de7e8 Mon Sep 17 00:00:00 2001 From: abmahmoud03 Date: Sun, 19 Oct 2025 02:52:53 -0400 Subject: [PATCH 093/102] fix(scripts): remove old performance testing script --- scripts/visualization_benchmark.py | 798 ----------------------------- 1 file changed, 798 deletions(-) delete mode 100644 scripts/visualization_benchmark.py diff --git a/scripts/visualization_benchmark.py b/scripts/visualization_benchmark.py deleted file mode 100644 index 5c1aebcb..00000000 --- a/scripts/visualization_benchmark.py +++ /dev/null @@ -1,798 +0,0 @@ -#!/usr/bin/env python3 -""" -Visualization Benchmark Script -=============================== - -A command-line tool for benchmarking visualization functions from the SPAC -package. This script measures the performance of various plotting functions -with different dataset sizes and configurations. - -Usage: - # Benchmark with a custom dataset - python visualization_benchmark.py --dataset path/to/data.pkl --visualizations boxplot histogram - - # Benchmark with generated random data - python visualization_benchmark.py --min-datapoints 1000 --max-datapoints 10000 \ - --increment 1000 --visualizations both --output-plot - -Author: SPAC Development Team -Date: October 2025 -""" - -import argparse -import sys -import time -import pickle -from pathlib import Path -from typing import Dict, List, Tuple - -import numpy as np -import pandas as pd -import anndata as ad -import matplotlib.pyplot as plt -from sklearn.datasets import make_blobs -from sklearn.preprocessing import StandardScaler - -# Import SPAC visualization functions -from spac.visualization import boxplot, boxplot_interactive - - -def parse_arguments() -> argparse.Namespace: - """ - Parse command-line arguments for the benchmark script. - - Returns - ------- - argparse.Namespace - Parsed command-line arguments containing benchmark configuration. - """ - parser = argparse.ArgumentParser( - description="Benchmark visualization functions from the SPAC package", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - # Benchmark with custom dataset - %(prog)s --dataset data.pkl --visualizations boxplot histogram --output-plot - - # Benchmark with a single dataset size - %(prog)s --size 5000 --visualizations both --output-plot - - # Benchmark with generated data range - %(prog)s --min-datapoints 1000 --max-datapoints 5000 --increment 1000 \\ - --visualizations both --output-plot - - # Run only histogram benchmarks with single size - %(prog)s --size 2000 --visualizations histogram - """ - ) - - # Dataset input options - data_group = parser.add_argument_group("Dataset Options") - data_group.add_argument( - "--dataset", - type=str, - default=None, - metavar="PATH", - help="Path to input dataset file (pickle or anndata-readable format). " - "If not provided, random datasets will be generated." - ) - - # Random data generation options - random_group = parser.add_argument_group( - "Random Data Generation Options", - "Used when --dataset is not provided" - ) - random_group.add_argument( - "--size", - type=int, - default=None, - metavar="N", - help="Single dataset size to benchmark. If specified, overrides " - "min/max/increment options." - ) - random_group.add_argument( - "--min-datapoints", - type=int, - default=1000, - metavar="N", - help="Minimum number of datapoints for generated datasets (default: 1000). " - "Ignored if --size is specified." - ) - random_group.add_argument( - "--max-datapoints", - type=int, - default=10000, - metavar="N", - help="Maximum number of datapoints for generated datasets (default: 10000). " - "Ignored if --size is specified." - ) - random_group.add_argument( - "--increment", - type=int, - default=1000, - metavar="N", - help="Increment step for datapoint sizes (default: 1000). " - "Ignored if --size is specified." - ) - - # Visualization options - viz_group = parser.add_argument_group("Visualization Options") - viz_group.add_argument( - "--visualizations", - type=str, - nargs="+", - choices=["boxplot", "histogram", "both"], - default=["both"], - metavar="TYPE", - help="Which visualizations to benchmark: boxplot, histogram, or both " - "(default: both)" - ) - - # Plot parameter options - plot_group = parser.add_argument_group( - "Plot Parameters", - "Optional parameters for visualization functions. If not specified, " - "sensible defaults will be auto-discovered from the dataset." - ) - plot_group.add_argument( - "--annotation", - type=str, - default=None, - metavar="NAME", - help="Annotation column to use for grouping (e.g., cell_type). " - "If not provided, the first categorical annotation will be used." - ) - plot_group.add_argument( - "--features", - type=str, - nargs="+", - default=None, - metavar="NAME", - help="Feature(s) to plot. Provide one or more feature names separated by spaces. " - "If not provided, the first 3 features will be used." - ) - plot_group.add_argument( - "--layer", - type=str, - default=None, - metavar="NAME", - help="Layer to use for plotting (e.g., normalized, log_transformed). " - "If not provided, the main data matrix (adata.X) will be used." - ) - - # Output options - output_group = parser.add_argument_group("Output Options") - output_group.add_argument( - "--output-plot", - action="store_true", - help="Generate and save a plot comparing benchmark results" - ) - output_group.add_argument( - "--output-dir", - type=str, - default="benchmark_results", - metavar="DIR", - help="Directory to save benchmark results (default: benchmark_results)" - ) - - # Parse arguments - args = parser.parse_args() - - # Validate arguments - _validate_arguments(args) - - return args - - -def _validate_arguments(args: argparse.Namespace) -> None: - """ - Validate the parsed command-line arguments. - - Parameters - ---------- - args : argparse.Namespace - Parsed command-line arguments to validate. - - Raises - ------ - ValueError - If any argument validation fails. - SystemExit - If validation fails, exits with error message. - """ - # Validate dataset path if provided - if args.dataset is not None: - dataset_path = Path(args.dataset) - if not dataset_path.exists(): - print(f"Error: Dataset file not found: {args.dataset}", file=sys.stderr) - sys.exit(1) - if not dataset_path.is_file(): - print(f"Error: Dataset path is not a file: {args.dataset}", file=sys.stderr) - sys.exit(1) - - # Validate random data generation parameters - if args.dataset is None: - if args.size is not None: - # Single size mode - if args.size <= 0: - print("Error: --size must be positive", file=sys.stderr) - sys.exit(1) - else: - # Range mode - if args.min_datapoints <= 0: - print("Error: --min-datapoints must be positive", file=sys.stderr) - sys.exit(1) - if args.max_datapoints <= 0: - print("Error: --max-datapoints must be positive", file=sys.stderr) - sys.exit(1) - if args.min_datapoints > args.max_datapoints: - print("Error: --min-datapoints cannot exceed --max-datapoints", - file=sys.stderr) - sys.exit(1) - if args.increment <= 0: - print("Error: --increment must be positive", file=sys.stderr) - sys.exit(1) - - # Normalize "both" in visualizations list - if "both" in args.visualizations: - args.visualizations = ["boxplot", "histogram"] - else: - # Remove duplicates while preserving order - args.visualizations = list(dict.fromkeys(args.visualizations)) - - -def _discover_plot_parameters(adata: ad.AnnData, args: argparse.Namespace) -> dict: - """ - Auto-discover sensible default parameters for plotting from the dataset. - - Parameters - ---------- - adata : ad.AnnData - The AnnData object to discover parameters from. - args : argparse.Namespace - Command-line arguments that may contain user-specified overrides. - - Returns - ------- - dict - Dictionary containing discovered/selected parameters: - - annotation: str or None - - features: list of str - - layer: str or None - """ - params = {} - - # Discover or use specified annotation - if args.annotation is not None: - # Validate user-specified annotation - if args.annotation not in adata.obs.columns: - print(f"Error: Annotation '{args.annotation}' not found in dataset.", - file=sys.stderr) - print(f"Available annotations: {', '.join(adata.obs.columns.tolist())}", - file=sys.stderr) - sys.exit(1) - params['annotation'] = args.annotation - print(f" Using specified annotation: {args.annotation}") - else: - # Auto-discover: use first categorical column - categorical_cols = [col for col in adata.obs.columns - if pd.api.types.is_categorical_dtype(adata.obs[col]) or - adata.obs[col].dtype == 'object'] - if categorical_cols: - params['annotation'] = categorical_cols[0] - print(f" Auto-discovered annotation: {params['annotation']}") - print(f" (Available: {', '.join(categorical_cols)})") - else: - params['annotation'] = None - print(" No categorical annotations found, will use None") - - # Discover or use specified features - if args.features is not None: - # Validate user-specified features - invalid_features = [f for f in args.features if f not in adata.var_names] - if invalid_features: - print(f"Error: Features not found in dataset: {', '.join(invalid_features)}", - file=sys.stderr) - print(f"Available features: {', '.join(adata.var_names[:10].tolist())}" - f"{'...' if len(adata.var_names) > 10 else ''}", - file=sys.stderr) - sys.exit(1) - params['features'] = args.features - print(f" Using specified features: {', '.join(args.features)}") - else: - # Auto-discover: use first 3 features (or all if fewer than 3) - n_features = min(3, len(adata.var_names)) - params['features'] = adata.var_names[:n_features].tolist() - print(f" Auto-discovered features: {', '.join(params['features'])}") - if len(adata.var_names) > 3: - print(f" (Total available: {len(adata.var_names)})") - - # Discover or use specified layer - if args.layer is not None: - # Validate user-specified layer - if args.layer not in adata.layers.keys(): - print(f"Error: Layer '{args.layer}' not found in dataset.", - file=sys.stderr) - available = ', '.join(adata.layers.keys()) if adata.layers.keys() else 'None' - print(f"Available layers: {available}", file=sys.stderr) - sys.exit(1) - params['layer'] = args.layer - print(f" Using specified layer: {args.layer}") - else: - # Auto-discover: use first layer if available, otherwise None (main matrix) - if len(adata.layers.keys()) > 0: - params['layer'] = list(adata.layers.keys())[0] - print(f" Auto-discovered layer: {params['layer']}") - print(f" (Available: {', '.join(adata.layers.keys())})") - else: - params['layer'] = None - print(" No layers found, will use main matrix (adata.X)") - - return params - - -def load_dataset(dataset_path: str) -> ad.AnnData: - """ - Load a dataset from file, supporting both pickle and anndata formats. - - Parameters - ---------- - dataset_path : str - Path to the dataset file. Supports pickle files (.pkl, .pickle) - and any format readable by anndata (e.g., .h5ad, .zarr, .loom). - - Returns - ------- - ad.AnnData - Loaded AnnData object containing the dataset. - - Raises - ------ - ValueError - If the file format is not supported or loading fails. - SystemExit - If the loaded object is not an AnnData instance. - """ - file_path = Path(dataset_path) - file_suffix = file_path.suffix.lower() - - print(f"Loading dataset from: {dataset_path}") - - try: - # Try loading as pickle file first for .pkl and .pickle extensions - if file_suffix in ['.pkl', '.pickle']: - print(" Detected pickle format, loading with pickle...") - with open(file_path, 'rb') as f: - adata = pickle.load(f) - - # Ensure the loaded object is an AnnData instance - if not isinstance(adata, ad.AnnData): - print(f"Error: Pickle file does not contain an AnnData object. " - f"Found type: {type(adata).__name__}", file=sys.stderr) - sys.exit(1) - - else: - # Try loading with anndata for other formats - print(f" Attempting to load with anndata...") - adata = ad.read(dataset_path) - - # Validate that we have a proper AnnData object - if not isinstance(adata, ad.AnnData): - print(f"Error: Loaded object is not an AnnData instance. " - f"Found type: {type(adata).__name__}", file=sys.stderr) - sys.exit(1) - - # Print basic dataset information - print(f" Successfully loaded dataset:") - print(f" Observations (cells): {adata.n_obs}") - print(f" Variables (features): {adata.n_vars}") - if adata.obs.columns.size > 0: - print(f" Annotations: {', '.join(adata.obs.columns[:5].tolist())}" - f"{'...' if len(adata.obs.columns) > 5 else ''}") - print() - - return adata - - except FileNotFoundError: - print(f"Error: File not found: {dataset_path}", file=sys.stderr) - sys.exit(1) - except pickle.UnpicklingError as e: - print(f"Error: Failed to unpickle file: {e}", file=sys.stderr) - sys.exit(1) - except Exception as e: - print(f"Error: Failed to load dataset: {e}", file=sys.stderr) - print(f" Supported formats: pickle (.pkl, .pickle) or anndata-readable " - f"formats (.h5ad, .zarr, .loom, etc.)", file=sys.stderr) - sys.exit(1) - - -def generate_random_dataset(n_obs: int, random_state: int = 42) -> ad.AnnData: - """ - Generate a random AnnData object with realistic clustering for benchmarking. - - Creates a synthetic dataset with 5 features, 5 annotations, and 3 data layers. - Uses sklearn's make_blobs to generate data with natural clustering patterns - rather than pure noise. - - Parameters - ---------- - n_obs : int - Number of observations (cells/datapoints) to generate. - random_state : int, optional - Random seed for reproducibility. Default is 42. - - Returns - ------- - ad.AnnData - Generated AnnData object with the following structure: - - X: Main feature matrix (n_obs × 5 features) - - obs: DataFrame with 5 categorical annotations - - var: Feature names (marker_1 through marker_5) - - layers: 3 transformed versions of the data - (normalized, log_transformed, scaled) - """ - np.random.seed(random_state) - - # Generate base data with natural clustering using make_blobs - # Create 5 features with 5 cluster centers for realistic structure - n_features = 5 - n_centers = 5 - - X, cluster_labels = make_blobs( - n_samples=n_obs, - n_features=n_features, - centers=n_centers, - cluster_std=1.5, - random_state=random_state - ) - - # Make values positive (common in biological data) and add some variation - X = np.abs(X) + np.random.exponential(scale=2.0, size=X.shape) - - # Create feature names - feature_names = [f"marker_{i+1}" for i in range(n_features)] - - # Create 5 annotations with varying numbers of unique values - # Assign based on clusters and add some randomness for realism - - # Annotation 1: cell_type (5 categories) - cell_types = [f"Type_{chr(65+i)}" for i in range(5)] # Type_A, Type_B, etc. - cell_type = np.array([cell_types[i % 5] for i in cluster_labels]) - - # Annotation 2: phenotype (4 categories) - partially correlated with clusters - phenotypes = [f"Pheno_{i+1}" for i in range(4)] - phenotype = np.array([phenotypes[i % 4] for i in cluster_labels]) - # Add some randomness to ~20% of assignments - random_mask = np.random.random(n_obs) < 0.2 - phenotype[random_mask] = np.random.choice(phenotypes, size=random_mask.sum()) - - # Annotation 3: region (3 categories) - more random - regions = ["Region_X", "Region_Y", "Region_Z"] - region = np.random.choice(regions, size=n_obs) - - # Annotation 4: batch (3 categories) - random assignment - batches = ["Batch_1", "Batch_2", "Batch_3"] - batch = np.random.choice(batches, size=n_obs) - - # Annotation 5: treatment (2 categories) - binary, roughly balanced - treatments = ["Control", "Treated"] - treatment = np.random.choice(treatments, size=n_obs, p=[0.5, 0.5]) - - # Create observations DataFrame - obs = pd.DataFrame({ - 'cell_type': pd.Categorical(cell_type), - 'phenotype': pd.Categorical(phenotype), - 'region': pd.Categorical(region), - 'batch': pd.Categorical(batch), - 'treatment': pd.Categorical(treatment) - }) - - # Create the base AnnData object - adata = ad.AnnData(X=X, obs=obs) - adata.var_names = feature_names - - # Create 3 layers with different transformations - - # Layer 1: Normalized (min-max normalization per feature) - X_normalized = np.zeros_like(X) - for i in range(n_features): - feature_min = X[:, i].min() - feature_max = X[:, i].max() - X_normalized[:, i] = (X[:, i] - feature_min) / (feature_max - feature_min) - adata.layers['normalized'] = X_normalized - - # Layer 2: Log-transformed (log1p to handle zeros) - adata.layers['log_transformed'] = np.log1p(X) - - # Layer 3: Scaled (standardized with mean=0, std=1) - scaler = StandardScaler() - adata.layers['scaled'] = scaler.fit_transform(X) - - return adata - - -def benchmark_boxplot( - adata: ad.AnnData, - annotation: str, - features: List[str], - layer: str = None -) -> Tuple[float, dict]: - """ - Benchmark the standard boxplot function. - - Parameters - ---------- - adata : ad.AnnData - The AnnData object to plot. - annotation : str - Annotation column for grouping. - features : List[str] - List of features to plot. - layer : str, optional - Layer to use for plotting. - - Returns - ------- - tuple - (execution_time, result_dict) where result_dict contains the plot outputs. - """ - # Close any existing plots to avoid memory issues - plt.close('all') - - start_time = time.time() - - try: - # Call boxplot function - fig, ax, df = boxplot( - adata, - annotation=annotation, - features=features, - layer=layer - ) - - execution_time = time.time() - start_time - - # Clean up to free memory - plt.close(fig) - - return execution_time, {'fig': fig, 'ax': ax, 'df': df} - - except Exception as e: - execution_time = time.time() - start_time - print(f" Error in boxplot: {e}") - return execution_time, {'error': str(e)} - - -def benchmark_boxplot_interactive( - adata: ad.AnnData, - annotation: str, - features: List[str], - layer: str = None -) -> Tuple[float, dict]: - """ - Benchmark the interactive boxplot function. - - Parameters - ---------- - adata : ad.AnnData - The AnnData object to plot. - annotation : str - Annotation column for grouping. - features : List[str] - List of features to plot. - layer : str, optional - Layer to use for plotting. - - Returns - ------- - tuple - (execution_time, result_dict) where result_dict contains the plot outputs. - """ - # Close any existing plots to avoid memory issues - plt.close('all') - - start_time = time.time() - - try: - # Call boxplot_interactive function with showfliers='downsample' - result = boxplot_interactive( - adata, - annotation=annotation, - features=features, - layer=layer, - showfliers='downsample' - ) - - execution_time = time.time() - start_time - - return execution_time, result - - except Exception as e: - execution_time = time.time() - start_time - print(f" Error in boxplot_interactive: {e}") - return execution_time, {'error': str(e)} - - -def run_boxplot_benchmarks( - datasets: List[ad.AnnData], - plot_params: dict -) -> pd.DataFrame: - """ - Run boxplot benchmarks on all provided datasets. - - Parameters - ---------- - datasets : List[ad.AnnData] - List of datasets to benchmark on. - plot_params : dict - Dictionary containing plot parameters (annotation, features, layer). - - Returns - ------- - pd.DataFrame - DataFrame containing benchmark results with columns: - - n_obs: number of observations - - boxplot_time: execution time for boxplot - - boxplot_interactive_time: execution time for boxplot_interactive - - speedup_factor: ratio of times (boxplot / boxplot_interactive) - """ - print("Running boxplot benchmarks...") - print("=" * 70) - - results = [] - - for i, adata in enumerate(datasets, 1): - n_obs = adata.n_obs - print(f"\nDataset {i}/{len(datasets)}: {n_obs} observations") - - # Benchmark standard boxplot - print(" Testing boxplot...") - boxplot_time, boxplot_result = benchmark_boxplot( - adata, - annotation=plot_params['annotation'], - features=plot_params['features'], - layer=plot_params['layer'] - ) - print(f" Time: {boxplot_time:.4f} seconds") - - # Benchmark interactive boxplot - print(" Testing boxplot_interactive...") - interactive_time, interactive_result = benchmark_boxplot_interactive( - adata, - annotation=plot_params['annotation'], - features=plot_params['features'], - layer=plot_params['layer'] - ) - print(f" Time: {interactive_time:.4f} seconds") - - # Calculate speedup factor - if interactive_time > 0: - speedup = boxplot_time / interactive_time - print(f" Speedup factor: {speedup:.2f}x " - f"({'boxplot_interactive' if speedup > 1 else 'boxplot'} is faster)") - else: - speedup = None - - results.append({ - 'n_obs': n_obs, - 'boxplot_time': boxplot_time, - 'boxplot_interactive_time': interactive_time, - 'speedup_factor': speedup - }) - - print("\n" + "=" * 70) - print("Benchmark complete!") - - return pd.DataFrame(results) - - -def main(): - """ - Main entry point for the benchmark script. - - Orchestrates the benchmarking process including argument parsing, - data loading/generation, running benchmarks, and outputting results. - """ - # Parse command-line arguments - args = parse_arguments() - - print("=" * 70) - print("SPAC Visualization Benchmark") - print("=" * 70) - print() - - # Display configuration - print(f"Configuration:") - print(f" Dataset: {args.dataset if args.dataset else 'Generated'}") - if args.dataset is None: - if args.size is not None: - print(f" Dataset size: {args.size}") - else: - print(f" Datapoint range: {args.min_datapoints} - {args.max_datapoints}") - print(f" Increment: {args.increment}") - print(f" Visualizations: {', '.join(args.visualizations)}") - print(f" Output plot: {args.output_plot}") - print(f" Output directory: {args.output_dir}") - print() - - # Step 1: Load or generate datasets - if args.dataset is not None: - # Load user-provided dataset - adata = load_dataset(args.dataset) - datasets = [adata] - else: - # Generate random datasets for benchmarking - print("Generating random datasets for benchmarking...") - datasets = [] - - if args.size is not None: - # Single size mode - dataset_sizes = [args.size] - else: - # Range mode - dataset_sizes = range( - args.min_datapoints, - args.max_datapoints + 1, - args.increment - ) - - for size in dataset_sizes: - print(f" Generating dataset with {size} observations...") - adata = generate_random_dataset(n_obs=size, random_state=42) - datasets.append(adata) - - print(f" Successfully generated {len(datasets)} dataset(s)") - if args.size is not None: - print(f" Size: {args.size}") - else: - print(f" Size range: {args.min_datapoints} to {args.max_datapoints}") - print() - - # Step 2: Auto-discover plot parameters from the first dataset - print("Discovering plot parameters...") - plot_params = _discover_plot_parameters(datasets[0], args) - print() - - print(f"Plot parameters to be used:") - print(f" Annotation: {plot_params['annotation']}") - print(f" Features: {', '.join(plot_params['features'])}") - print(f" Layer: {plot_params['layer']}") - print() - - # Step 3: Run benchmarks based on selected visualizations - all_results = {} - - if 'boxplot' in args.visualizations: - # Run boxplot vs boxplot_interactive benchmark - boxplot_results = run_boxplot_benchmarks(datasets, plot_params) - all_results['boxplot'] = boxplot_results - - # Display summary - print("\nBoxplot Benchmark Summary:") - print(boxplot_results.to_string(index=False)) - print() - - # TODO: Add histogram benchmarks when requested - - # Step 4: Save results to output directory - output_dir = Path(args.output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - - for viz_type, results_df in all_results.items(): - output_file = output_dir / f"{viz_type}_benchmark_results.csv" - results_df.to_csv(output_file, index=False) - print(f"Results saved to: {output_file}") - - # Step 5: Generate comparison plots if requested - if args.output_plot and len(all_results) > 0: - print("\nGenerating comparison plots...") - # TODO: Implement plot generation - print(" Plot generation not yet implemented") - - print("\n" + "=" * 70) - print("Benchmark execution complete!") - print("=" * 70) - - -if __name__ == "__main__": - main() From c31c3ffec708bf2d2cf0a9ce487f54ccd04fe874 Mon Sep 17 00:00:00 2001 From: abmahmoud03 Date: Mon, 20 Oct 2025 07:53:16 -0700 Subject: [PATCH 094/102] fix(performance_test): fix the speedup calculation logic --- tests/test_performance/test_boxplot_performance.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_performance/test_boxplot_performance.py b/tests/test_performance/test_boxplot_performance.py index db6754dc..37086b13 100644 --- a/tests/test_performance/test_boxplot_performance.py +++ b/tests/test_performance/test_boxplot_performance.py @@ -170,9 +170,9 @@ def _run_comparison(self, adata, test_name): print(f" Speedup factor: {speedup:.2f}x") if speedup > 1: - print(f" → boxplot is {speedup:.2f}x faster") + print(f" → boxplot_interactive is {speedup:.2f}x faster") elif speedup < 1: - print(f" → boxplot_interactive is {1/speedup:.2f}x faster") + print(f" → boxplot is {1/speedup:.2f}x faster") else: print(f" → Both functions have similar performance") From 179482eb16bbfaa7566bfcc8aae0adb29c0d2429 Mon Sep 17 00:00:00 2001 From: abmahmoud03 Date: Tue, 21 Oct 2025 07:46:19 -0700 Subject: [PATCH 095/102] fix(histogram_performance): add clarifying comment for old hist implementation --- tests/test_performance/test_histogram_performance.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_performance/test_histogram_performance.py b/tests/test_performance/test_histogram_performance.py index 5cc6c14b..38f496b4 100644 --- a/tests/test_performance/test_histogram_performance.py +++ b/tests/test_performance/test_histogram_performance.py @@ -128,7 +128,12 @@ def tearDown(self): def histogram_old(adata, feature=None, annotation=None, layer=None, group_by=None, together=False, ax=None, x_log_scale=False, y_log_scale=False, **kwargs): - """Old histogram implementation for performance comparison.""" + """ + Old histogram implementation for performance comparison. + + Copied from commit 1cfad52f00aa6c1b8384f727b60e3bf07f57bee6 in + visualization.py, before the refactor to histogram + """ # If no feature or annotation is specified, apply default behavior if feature is None and annotation is None: feature = adata.var_names[0] From 4e083fbe77a1aaa1dac1d5b3d7841ed172721132 Mon Sep 17 00:00:00 2001 From: George Zaki Date: Thu, 30 Oct 2025 13:02:50 -0400 Subject: [PATCH 096/102] fix(nearest_neighbor_template): Break the title in two lines --- .../visualize_nearest_neighbor_template.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/spac/templates/visualize_nearest_neighbor_template.py b/src/spac/templates/visualize_nearest_neighbor_template.py index c5719509..34bc0338 100644 --- a/src/spac/templates/visualize_nearest_neighbor_template.py +++ b/src/spac/templates/visualize_nearest_neighbor_template.py @@ -93,7 +93,7 @@ def run_from_json( fig_dpi = params.get("Figure_DPI", 300) global_font_size = params.get("Font_Size", 12) fig_title = ( - f'Nearest Neighbor Distance Distribution Measured from ' + f'Nearest Neighbor Distance Distribution\nMeasured from ' f'"{source_label}"' ) @@ -135,16 +135,16 @@ def run_from_json( ) print(warning_message) facet_plot = False - + result_dict = visualize_nearest_neighbor( adata=adata, annotation=annotation, - spatial_distance=distance_key, + spatial_distance=distance_key, distance_from=source_label, distance_to=distance_to_processed, method=method, plot_type=plot_type, - stratify_by=image_id, + stratify_by=image_id, facet_plot=facet_plot, log=log_scale, annotation_colorscale=annotation_colorscale, @@ -220,7 +220,7 @@ def _flatten_axes(ax_input): isinstance(figs_out, plt.Figure)): for ax_item in flat_axes_list: ax_item.set_xlabel('') - + sup_ha_align = 'center' if 0 < x_axis_title_rotation % 360 < 180: sup_ha_align = 'right' @@ -317,7 +317,7 @@ def _label_each_figure(fig_list, categories): # Track figures for optional saving figures = [] - + if isinstance(figs_out, list) and not facet_plot and \ cat_list and len(figs_out) == len(cat_list): # Scenario: Multiple separate figures, one per category @@ -336,7 +336,7 @@ def _label_each_figure(fig_list, categories): for fig_item_to_display in figures_to_display: if fig_item_to_display is not None: _title_main(fig_item_to_display, fig_title) - + bottom_padding = 0.01 # Make space for shared x-title if fig_item_to_display is shared_x_title_applied_to_fig: @@ -350,7 +350,7 @@ def _label_each_figure(fig_list, categories): ) if show_plot: plt.show() - + # summary statistics # 1) Per-group summary df_summary_group = ( @@ -391,7 +391,7 @@ def _label_each_figure(fig_list, categories): "Output_File", "nearest_neighbor_plots.csv" ) saved_files = save_outputs({output_file: final_df}) - + print(f"\nSaved summary statistics to '{output_file}'.") print( f"Visualize Nearest Neighbor completed → " @@ -426,4 +426,4 @@ def _label_each_figure(fig_list, categories): for filename, filepath in result.items(): print(f" {filename}: {filepath}") else: - print("\nReturned figure(s) and dataframe") \ No newline at end of file + print("\nReturned figure(s) and dataframe") From fc664ad96b3375a3255f9bb36e51d0e4a505daba Mon Sep 17 00:00:00 2001 From: George Zaki Date: Thu, 30 Oct 2025 13:34:03 -0400 Subject: [PATCH 097/102] test(perforamnce): skip performance tests by default --- .../test_boxplot_performance.py | 60 +++++++++-------- .../test_histogram_performance.py | 64 +++++++++++-------- 2 files changed, 69 insertions(+), 55 deletions(-) diff --git a/tests/test_performance/test_boxplot_performance.py b/tests/test_performance/test_boxplot_performance.py index 37086b13..0cb7b54a 100644 --- a/tests/test_performance/test_boxplot_performance.py +++ b/tests/test_performance/test_boxplot_performance.py @@ -12,6 +12,12 @@ matplotlib.use('Agg') # Set the backend to 'Agg' to suppress plot window +skip_perf = unittest.skipUnless( + os.getenv("SPAC_RUN_PERF") == "1", + "Perf tests disabled by default" +) + +@skip_perf class TestBoxplotPerformance(unittest.TestCase): """Performance comparison tests for boxplot vs boxplot_interactive.""" @@ -21,19 +27,19 @@ def setUpClass(cls): print("\n" + "=" * 70) print("Setting up large datasets for boxplot performance tests...") print("=" * 70) - + # Generate 1M cell dataset print("\nGenerating 1M cell dataset...") start = time.time() cls.adata_1m = cls._generate_dataset(n_obs=1_000_000, random_state=42) print(f" Completed in {time.time() - start:.2f} seconds") - + # Generate 5M cell dataset print("\nGenerating 5M cell dataset...") start = time.time() cls.adata_5m = cls._generate_dataset(n_obs=5_000_000, random_state=42) print(f" Completed in {time.time() - start:.2f} seconds") - + # Generate 10M cell dataset print("\nGenerating 10M cell dataset...") start = time.time() @@ -45,18 +51,18 @@ def setUpClass(cls): def _generate_dataset(n_obs: int, random_state: int = 42) -> ad.AnnData: """ Generate a synthetic AnnData object with realistic clustering. - + Creates dataset with: - 5 features (marker_1 to marker_5) - 5 annotations (cell_type, phenotype, region, batch, treatment) - 3 layers (normalized, log_transformed, scaled) """ np.random.seed(random_state) - + # Generate base data with natural clustering n_features = 5 n_centers = 5 - + X, cluster_labels = make_blobs( n_samples=n_obs, n_features=n_features, @@ -64,31 +70,31 @@ def _generate_dataset(n_obs: int, random_state: int = 42) -> ad.AnnData: cluster_std=1.5, random_state=random_state ) - + # Make values positive and add variation X = np.abs(X) + np.random.exponential(scale=2.0, size=X.shape) - + # Create feature names feature_names = [f"marker_{i+1}" for i in range(n_features)] - + # Create annotations based on clusters cell_types = [f"Type_{chr(65+i)}" for i in range(5)] cell_type = np.array([cell_types[i % 5] for i in cluster_labels]) - + phenotypes = [f"Pheno_{i+1}" for i in range(4)] phenotype = np.array([phenotypes[i % 4] for i in cluster_labels]) random_mask = np.random.random(n_obs) < 0.2 phenotype[random_mask] = np.random.choice(phenotypes, size=random_mask.sum()) - + regions = ["Region_X", "Region_Y", "Region_Z"] region = np.random.choice(regions, size=n_obs) - + batches = ["Batch_1", "Batch_2", "Batch_3"] batch = np.random.choice(batches, size=n_obs) - + treatments = ["Control", "Treated"] treatment = np.random.choice(treatments, size=n_obs, p=[0.5, 0.5]) - + # Create observations DataFrame obs = pd.DataFrame({ 'cell_type': pd.Categorical(cell_type), @@ -97,11 +103,11 @@ def _generate_dataset(n_obs: int, random_state: int = 42) -> ad.AnnData: 'batch': pd.Categorical(batch), 'treatment': pd.Categorical(treatment) }) - + # Create AnnData object adata = ad.AnnData(X=X, obs=obs) adata.var_names = feature_names - + # Create layers with different transformations X_normalized = np.zeros_like(X) for i in range(n_features): @@ -109,12 +115,12 @@ def _generate_dataset(n_obs: int, random_state: int = 42) -> ad.AnnData: feature_max = X[:, i].max() X_normalized[:, i] = (X[:, i] - feature_min) / (feature_max - feature_min) adata.layers['normalized'] = X_normalized - + adata.layers['log_transformed'] = np.log1p(X) - + scaler = StandardScaler() adata.layers['scaled'] = scaler.fit_transform(X) - + return adata def tearDown(self): @@ -127,14 +133,14 @@ def _run_comparison(self, adata, test_name): features = ['marker_1', 'marker_2', 'marker_3', 'marker_4', 'marker_5'] annotation = 'cell_type' layer = 'normalized' - + print(f"\n{'=' * 70}") print(f"{test_name}: {n_obs:,} cells") print(f" Features: {', '.join(features)}") print(f" Annotation: {annotation}") print(f" Layer: {layer}") print(f"{'=' * 70}") - + # Test boxplot print("\n Running boxplot...") start = time.time() @@ -147,7 +153,7 @@ def _run_comparison(self, adata, test_name): boxplot_time = time.time() - start print(f" Time: {boxplot_time:.2f} seconds") plt.close('all') - + # Test boxplot_interactive with downsampling print("\n Running boxplot_interactive (with downsampling)...") start = time.time() @@ -160,24 +166,24 @@ def _run_comparison(self, adata, test_name): ) interactive_time = time.time() - start print(f" Time: {interactive_time:.2f} seconds") - + # Calculate speedup speedup = boxplot_time / interactive_time if interactive_time > 0 else 0 - + print(f"\n Results:") print(f" boxplot: {boxplot_time:.2f}s") print(f" boxplot_interactive: {interactive_time:.2f}s") print(f" Speedup factor: {speedup:.2f}x") - + if speedup > 1: print(f" → boxplot_interactive is {speedup:.2f}x faster") elif speedup < 1: print(f" → boxplot is {1/speedup:.2f}x faster") else: print(f" → Both functions have similar performance") - + print(f"{'=' * 70}\n") - + # Store results for potential further analysis return { 'n_obs': n_obs, diff --git a/tests/test_performance/test_histogram_performance.py b/tests/test_performance/test_histogram_performance.py index 38f496b4..458fbbe5 100644 --- a/tests/test_performance/test_histogram_performance.py +++ b/tests/test_performance/test_histogram_performance.py @@ -15,6 +15,14 @@ matplotlib.use('Agg') # Set the backend to 'Agg' to suppress plot window + + +skip_perf = unittest.skipUnless( + os.getenv("SPAC_RUN_PERF") == "1", + "Perf tests disabled by default" +) + +@skip_perf class TestHistogramPerformance(unittest.TestCase): """Performance comparison tests for histogram vs histogram_old.""" @@ -24,19 +32,19 @@ def setUpClass(cls): print("\n" + "=" * 70) print("Setting up large datasets for histogram performance tests...") print("=" * 70) - + # Generate 1M cell dataset print("\nGenerating 1M cell dataset...") start = time.time() cls.adata_1m = cls._generate_dataset(n_obs=1_000_000, random_state=42) print(f" Completed in {time.time() - start:.2f} seconds") - + # Generate 5M cell dataset print("\nGenerating 5M cell dataset...") start = time.time() cls.adata_5m = cls._generate_dataset(n_obs=5_000_000, random_state=42) print(f" Completed in {time.time() - start:.2f} seconds") - + # Generate 10M cell dataset print("\nGenerating 10M cell dataset...") start = time.time() @@ -48,18 +56,18 @@ def setUpClass(cls): def _generate_dataset(n_obs: int, random_state: int = 42) -> ad.AnnData: """ Generate a synthetic AnnData object with realistic clustering. - + Creates dataset with: - 5 features (marker_1 to marker_5) - 5 annotations (cell_type, phenotype, region, batch, treatment) - 3 layers (normalized, log_transformed, scaled) """ np.random.seed(random_state) - + # Generate base data with natural clustering n_features = 5 n_centers = 5 - + X, cluster_labels = make_blobs( n_samples=n_obs, n_features=n_features, @@ -67,31 +75,31 @@ def _generate_dataset(n_obs: int, random_state: int = 42) -> ad.AnnData: cluster_std=1.5, random_state=random_state ) - + # Make values positive and add variation X = np.abs(X) + np.random.exponential(scale=2.0, size=X.shape) - + # Create feature names feature_names = [f"marker_{i+1}" for i in range(n_features)] - + # Create annotations based on clusters cell_types = [f"Type_{chr(65+i)}" for i in range(5)] cell_type = np.array([cell_types[i % 5] for i in cluster_labels]) - + phenotypes = [f"Pheno_{i+1}" for i in range(4)] phenotype = np.array([phenotypes[i % 4] for i in cluster_labels]) random_mask = np.random.random(n_obs) < 0.2 phenotype[random_mask] = np.random.choice(phenotypes, size=random_mask.sum()) - + regions = ["Region_X", "Region_Y", "Region_Z"] region = np.random.choice(regions, size=n_obs) - + batches = ["Batch_1", "Batch_2", "Batch_3"] batch = np.random.choice(batches, size=n_obs) - + treatments = ["Control", "Treated"] treatment = np.random.choice(treatments, size=n_obs, p=[0.5, 0.5]) - + # Create observations DataFrame obs = pd.DataFrame({ 'cell_type': pd.Categorical(cell_type), @@ -100,11 +108,11 @@ def _generate_dataset(n_obs: int, random_state: int = 42) -> ad.AnnData: 'batch': pd.Categorical(batch), 'treatment': pd.Categorical(treatment) }) - + # Create AnnData object adata = ad.AnnData(X=X, obs=obs) adata.var_names = feature_names - + # Create layers with different transformations X_normalized = np.zeros_like(X) for i in range(n_features): @@ -112,12 +120,12 @@ def _generate_dataset(n_obs: int, random_state: int = 42) -> ad.AnnData: feature_max = X[:, i].max() X_normalized[:, i] = (X[:, i] - feature_min) / (feature_max - feature_min) adata.layers['normalized'] = X_normalized - + adata.layers['log_transformed'] = np.log1p(X) - + scaler = StandardScaler() adata.layers['scaled'] = scaler.fit_transform(X) - + return adata def tearDown(self): @@ -131,7 +139,7 @@ def histogram_old(adata, feature=None, annotation=None, layer=None, """ Old histogram implementation for performance comparison. - Copied from commit 1cfad52f00aa6c1b8384f727b60e3bf07f57bee6 in + Copied from commit 1cfad52f00aa6c1b8384f727b60e3bf07f57bee6 in visualization.py, before the refactor to histogram """ # If no feature or annotation is specified, apply default behavior @@ -301,14 +309,14 @@ def _run_comparison(self, adata, test_name): feature = 'marker_1' annotation = None layer = 'normalized' - + print(f"\n{'=' * 70}") print(f"{test_name}: {n_obs:,} cells") print(f" Feature: {feature}") print(f" Annotation: {annotation}") print(f" Layer: {layer}") print(f"{'=' * 70}") - + # Test histogram_old print("\n Running histogram_old...") start = time.time() @@ -321,7 +329,7 @@ def _run_comparison(self, adata, test_name): old_time = time.time() - start print(f" Time: {old_time:.2f} seconds") plt.close('all') - + # Test histogram from SPAC print("\n Running histogram (SPAC)...") start = time.time() @@ -334,24 +342,24 @@ def _run_comparison(self, adata, test_name): new_time = time.time() - start print(f" Time: {new_time:.2f} seconds") plt.close('all') - + # Calculate speedup speedup = old_time / new_time if new_time > 0 else 0 - + print(f"\n Results:") print(f" histogram_old: {old_time:.2f}s") print(f" histogram: {new_time:.2f}s") print(f" Speedup factor: {speedup:.2f}x") - + if speedup > 1: print(f" → histogram (SPAC) is {speedup:.2f}x faster") elif speedup < 1: print(f" → histogram_old is {1/speedup:.2f}x faster") else: print(f" → Both functions have similar performance") - + print(f"{'=' * 70}\n") - + # Store results for potential further analysis return { 'n_obs': n_obs, From 9c4d606b9baf125dd5084125eb29e439b1930ab4 Mon Sep 17 00:00:00 2001 From: George Zaki Date: Thu, 30 Oct 2025 15:59:19 -0400 Subject: [PATCH 098/102] fix(test_performance): set the path to include spac --- tests/test_performance/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_performance/__init__.py b/tests/test_performance/__init__.py index f4b89e27..8042e032 100644 --- a/tests/test_performance/__init__.py +++ b/tests/test_performance/__init__.py @@ -1 +1,3 @@ -"""Performance tests for SPAC visualization functions.""" +import os +import sys +sys.path.append(os.path.dirname(os.path.realpath(__file__)) + "/../../src") From 800511118ab5970754b4e09bf7017b423328da92 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:36:56 -0500 Subject: [PATCH 099/102] feat: refactor all templates and unit tests - Refactored all template run_from_json() functions to use centralized save_results from template_utils - Added show_static_image toggle (default False) to relational_heatmap_template and sankey_plot_template to prevent Plotly-to-PNG hang on Galaxy - Refactored all unit tests in tests/templates/ using snowball approach: real data, real filesystem, no mocking - One test file per template validating output file existence, naming conventions, and non-empty artifacts - Updated posit_it_python_template to use centralized save_results Templates changed: 43 files in src/spac/templates/ Tests changed: 37 files in tests/templates/ --- .../templates/analysis_to_csv_template.py | 118 +++- .../templates/append_annotation_template.py | 149 +++-- .../append_pin_color_rule_template.py | 168 +++++ .../arcsinh_normalization_template.py | 131 +++- ...nary_to_categorical_annotation_template.py | 145 +++- src/spac/templates/boxplot_template.py | 176 +++-- .../templates/calculate_centroid_template.py | 147 +++- .../templates/combine_annotations_template.py | 134 +++- .../templates/combine_dataframes_template.py | 170 +++-- .../templates/downsample_cells_template.py | 151 +++-- .../hierarchical_heatmap_template.py | 65 +- src/spac/templates/histogram_template.py | 139 +++- .../interactive_spatial_plot_template.py | 122 +++- src/spac/templates/load_csv_files_template.py | 94 +++ .../templates/manual_phenotyping_template.py | 181 ++++- .../nearest_neighbor_calculation_template.py | 147 ++-- .../neighborhood_profile_template.py | 85 ++- .../templates/normalize_batch_template.py | 134 +++- .../phenograph_clustering_template.py | 99 ++- .../templates/posit_it_python_template.py | 245 ++++--- .../templates/quantile_scaling_template.py | 133 ++-- .../templates/relational_heatmap_template.py | 319 +++++---- src/spac/templates/rename_labels_template.py | 107 ++- .../ripley_l_calculation_template.py | 151 +++++ src/spac/templates/sankey_plot_template.py | 279 +++++--- src/spac/templates/select_values_template.py | 152 +++-- src/spac/templates/setup_analysis_template.py | 147 +++- .../templates/spatial_interaction_template.py | 137 ++-- src/spac/templates/spatial_plot_template.py | 186 ++++-- .../templates/subset_analysis_template.py | 148 ++++- ...ummarize_annotation_statistics_template.py | 118 +++- .../templates/summarize_dataframe_template.py | 164 +++-- src/spac/templates/template_utils.py | 442 +++++++++---- src/spac/templates/tsne_analysis_template.py | 105 ++- .../templates/umap_transformation_template.py | 102 ++- .../umap_tsne_pca_visualization_template.py | 242 +++++++ .../templates/utag_clustering_template.py | 100 ++- .../visualize_nearest_neighbor_template.py | 220 ++++-- .../templates/visualize_ripley_l_template.py | 155 +++++ .../z_score_normalization_template.py | 181 +++++ tests/templates/test_add_pin_color_rule.py | 173 ++--- .../test_analysis_to_csv_template.py | 157 ++--- .../test_append_annotation_template.py | 267 +++----- .../test_arcsinh_normalization_template.py | 180 ++--- ...nary_to_categorical_annotation_template.py | 215 +++--- tests/templates/test_boxplot_template.py | 501 ++++---------- .../test_calculate_centroid_template.py | 206 +++--- .../test_combine_annotations_template.py | 236 ++----- .../test_combine_dataframes_template.py | 210 +++--- .../test_downsample_cells_template.py | 207 +++--- .../test_hierarchical_heatmap_template.py | 231 ++----- tests/templates/test_histogram_template.py | 252 ++----- .../test_interactive_spatial_plot_template.py | 211 ++---- .../test_load_csv_files_with_config.py | 303 ++------- .../test_manual_phenotyping_template.py | 328 +++------ ...t_nearest_neighbor_calculation_template.py | 179 ++--- .../test_neighborhood_profile_template.py | 221 ++----- .../test_normalize_batch_template.py | 173 ++--- .../test_phenograph_clustering_template.py | 170 ++--- .../test_posit_it_python_template.py | 191 +++--- .../test_quantile_scaling_template.py | 234 ++----- .../test_relational_heatmap_template.py | 277 +++----- .../templates/test_rename_labels_template.py | 187 ++---- tests/templates/test_ripley_l_template.py | 261 ++------ tests/templates/test_sankey_plot_template.py | 241 +++---- .../templates/test_select_values_template.py | 184 ++--- .../templates/test_setup_analysis_template.py | 328 ++------- .../test_spatial_interaction_template.py | 252 ++----- tests/templates/test_spatial_plot_template.py | 281 ++------ .../test_subset_analysis_template.py | 167 ++--- ...ummarize_annotation_statistics_template.py | 187 ++---- .../test_summarize_dataframe_template.py | 268 ++------ tests/templates/test_template_utils.py | 626 +++++++++++++++--- .../templates/test_tsne_analysis_template.py | 154 ++--- .../test_umap_transformation_template.py | 203 ++---- .../templates/test_umap_tsne_pca_template.py | 206 ++---- .../test_utag_clustering_template.py | 182 ++--- ...est_visualize_nearest_neighbor_template.py | 279 +++----- .../test_visualize_ripley_template.py | 269 +++----- .../test_zscore_normalization_template.py | 233 ++----- 80 files changed, 8174 insertions(+), 7844 deletions(-) create mode 100644 src/spac/templates/append_pin_color_rule_template.py create mode 100644 src/spac/templates/load_csv_files_template.py create mode 100644 src/spac/templates/ripley_l_calculation_template.py create mode 100644 src/spac/templates/umap_tsne_pca_visualization_template.py create mode 100644 src/spac/templates/visualize_ripley_l_template.py create mode 100644 src/spac/templates/z_score_normalization_template.py diff --git a/src/spac/templates/analysis_to_csv_template.py b/src/spac/templates/analysis_to_csv_template.py index 8a2f35d8..b079439e 100644 --- a/src/spac/templates/analysis_to_csv_template.py +++ b/src/spac/templates/analysis_to_csv_template.py @@ -2,6 +2,9 @@ Platform-agnostic Analysis to CSV template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.analysis_to_csv_template import run_from_json @@ -9,6 +12,7 @@ """ import json import sys +import logging from pathlib import Path from typing import Any, Dict, Union import pandas as pd @@ -19,15 +23,18 @@ from spac.utils import check_table from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, text_to_value, ) +logger = logging.getLogger(__name__) + def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], pd.DataFrame]: """ Execute Analysis to CSV analysis with parameters from JSON. @@ -36,33 +43,59 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the dataframe + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Table_to_Export": "Original", + "Save_as_CSV_File": false, + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If False, returns the dataframe directly for in-memory workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. Returns ------- dict or DataFrame - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed DataFrame + If save_to_disk=True: Dictionary of saved file paths with structure: + {"dataframe": "path/to/dataframe.csv"} + If save_to_disk=False: The processed DataFrame + + Notes + ----- + Output Structure: + - DataFrame is saved as a CSV file when save_to_disk is True + - Otherwise, the DataFrame is returned for programmatic use """ # Parse parameters from JSON params = parse_params(json_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + if "outputs" not in params: + params["outputs"] = { + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + # Load the upstream analysis data adata = load_input(params["Upstream_Analysis"]) # Extract parameters input_layer = params.get("Table_to_Export", "Original") - save_file = params.get("Save_as_CSV_File", False) if input_layer == "Original": input_layer = None - def export_layer_to_csv( - adata, - layer=None): + def export_layer_to_csv(adata, layer=None): """ Exports the specified layer or the default .X data matrix of an AnnData object to a CSV file. @@ -86,7 +119,6 @@ def export_layer_to_csv( full_data_df = data_to_export.join(adata.obs) # Join the spatial coordinates - # Extract the spatial coordinates spatial_df = pd.DataFrame( adata.obsm['spatial'], @@ -97,41 +129,71 @@ def export_layer_to_csv( # Join spatial_df with full_data_df full_data_df = full_data_df.join(spatial_df) - return(full_data_df) + return full_data_df csv_data = export_layer_to_csv( adata=adata, layer=input_layer ) - - # Handle results based on save_results flag and save_file parameter - if save_results and save_file: - # Save outputs - output_file = params.get("Output_File", "analysis.csv") - saved_files = save_outputs({output_file: csv_data}) - - print(f"Analysis to CSV completed → {saved_files[output_file]}") + + logger.info(f"Exported DataFrame shape: {csv_data.shape}") + + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + if "dataframe" in params["outputs"]: + results_dict["dataframe"] = csv_data + + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + logger.info("Analysis to CSV completed successfully.") return saved_files else: - print(csv_data.info()) # Return the dataframe directly for in-memory workflows + logger.info("Returning DataFrame for in-memory use") return csv_data # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python analysis_to_csv_template.py ", + "Usage: python analysis_to_csv_template.py " + "[output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nReturned DataFrame") \ No newline at end of file + print("\nReturned DataFrame") + print(f"DataFrame shape: {result.shape}") diff --git a/src/spac/templates/append_annotation_template.py b/src/spac/templates/append_annotation_template.py index 1541cf0b..1d51a83c 100644 --- a/src/spac/templates/append_annotation_template.py +++ b/src/spac/templates/append_annotation_template.py @@ -2,6 +2,9 @@ Platform-agnostic Append Annotation template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.append_annotation_template import run_from_json @@ -12,7 +15,7 @@ from pathlib import Path from typing import Any, Dict, Union import pandas as pd -import pickle +import logging # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) @@ -20,16 +23,15 @@ from spac.data_utils import append_annotation from spac.utils import check_column_name from spac.templates.template_utils import ( - load_input, - save_outputs, + save_results, parse_params, - text_to_value, ) def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], pd.DataFrame]: """ Execute Append Annotation analysis with parameters from JSON. @@ -38,36 +40,79 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the dataframe + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Dataset": "path/to/dataframe.csv", + "Annotation_Pair_List": ["column1:value1", "column2:value2"], + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If True, saves the DataFrame with + appended annotations to a CSV file. If False, returns the DataFrame directly for in-memory workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. All outputs will be saved relative to this directory. Returns ------- dict or DataFrame - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed DataFrame + If save_to_disk=True: Dictionary of saved file paths with structure: + { + "dataframe": "path/to/dataframe.csv" + } + If save_to_disk=False: The processed DataFrame with appended annotations + + Notes + ----- + Output Structure: + - DataFrame is saved as a single CSV file + - When save_to_disk=False, the DataFrame is returned for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["dataframe"]) # Path to saved CSV file + + >>> # Get results in memory + >>> annotated_df = run_from_json("params.json", save_to_disk=False) + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") """ # Parse parameters from JSON params = parse_params(json_path) - # Load upstream data - could be DataFrame, CSV, or pickle + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + if "outputs" not in params: + params["outputs"] = { + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + + # Load upstream data - DataFrame or CSV file upstream_dataset = params["Upstream_Dataset"] if isinstance(upstream_dataset, pd.DataFrame): - # Direct DataFrame from previous step - input_dataframe = upstream_dataset + input_dataframe = upstream_dataset # Direct DataFrame from previous step elif isinstance(upstream_dataset, (str, Path)): + # Galaxy passes .dat files, but they contain CSV data + # Don't check extension - directly read as CSV path = Path(upstream_dataset) - if path.suffix.lower() == '.csv': + try: input_dataframe = pd.read_csv(path) - elif path.suffix.lower() in ['.pickle', '.pkl', '.p']: - with open(path, 'rb') as f: - input_dataframe = pickle.load(f) - else: + logging.info(f"Successfully loaded CSV data from: {path}") + except Exception as e: raise ValueError( - f"Unsupported file format: {path.suffix}. " - f"Supported formats: .csv, .pickle, .pkl" + f"Failed to read CSV data from '{path}'. " + f"This tool expects CSV/tabular format. " + f"Error: {str(e)}" ) else: raise TypeError( @@ -91,49 +136,73 @@ def run_from_json( # Add the key-value pair to the dictionary parsed_dict[key] = value - print(f"The pairs to add are:\n{parsed_dict}") + logging.info(f"The pairs to add are:\n{parsed_dict}") output_dataframe = append_annotation( input_dataframe, parsed_dict ) - print(output_dataframe.info()) + logging.info(output_dataframe.info()) - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "append_observations.csv") - # Ensure CSV extension for DataFrame output - if not output_file.endswith('.csv'): - output_file = output_file + '.csv' + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} - saved_files = save_outputs({output_file: output_dataframe}) + # Check for dataframe output + if "dataframe" in params["outputs"]: + results_dict["dataframe"] = output_dataframe - print( - f"Append Annotation completed → {saved_files[output_file]}" + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir ) + + logging.info("Append Annotation analysis completed successfully.") return saved_files else: - # Return the dataframe directly for in-memory workflows - print("Returning DataFrame (not saving to file)") + # Return the DataFrame directly for in-memory workflows + logging.info("Returning DataFrame for in-memory use") return output_dataframe # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python append_annotation_template.py ", + "Usage: python append_annotation_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + # Display results based on return type if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nReturned DataFrame") \ No newline at end of file + print("\nReturned DataFrame") + print(f"DataFrame shape: {result.shape}") diff --git a/src/spac/templates/append_pin_color_rule_template.py b/src/spac/templates/append_pin_color_rule_template.py new file mode 100644 index 00000000..eb9f06d8 --- /dev/null +++ b/src/spac/templates/append_pin_color_rule_template.py @@ -0,0 +1,168 @@ +""" +Platform-agnostic Append Pin Color Rule template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + +Usage +----- +>>> from spac.templates.add_pin_color_rule_template import run_from_json +>>> run_from_json("examples/add_pin_color_rule_params.json") +""" +import json +import sys +import logging +from pathlib import Path +from typing import Any, Dict, Union + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.data_utils import add_pin_color_rules +from spac.templates.template_utils import ( + load_input, + save_results, + parse_params, + string_list_to_dictionary, +) + +logger = logging.getLogger(__name__) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_to_disk: bool = True, + output_dir: str = None, +) -> Union[Dict[str, str], Any]: + """ + Execute Append Pin Color Rule analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Label_Color_Map": ["label1:red", "label2:blue"], + "Color_Map_Name": "_spac_colors", + "Overwrite_Previous_Color_Map": true, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If False, returns the adata object + directly for in-memory workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. + + Returns + ------- + dict or AnnData + If save_to_disk=True: Dictionary of saved file paths with structure: + {"analysis": "path/to/output.pickle"} + If save_to_disk=False: The processed AnnData object + + Notes + ----- + Output Structure: + - Analysis output is saved as a single pickle file + - When save_to_disk=False, the AnnData object is returned for programmatic use + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + if "outputs" not in params: + params["outputs"] = { + "analysis": {"type": "file", "name": "output.pickle"} + } + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + color_dict_string_list = params.get("Label_Color_Map", []) + color_map_name = params.get("Color_Map_Name", "_spac_colors") + overwrite = params.get("Overwrite_Previous_Color_Map", True) + + color_dict = string_list_to_dictionary( + color_dict_string_list, + key_name="label", + value_name="color" + ) + + add_pin_color_rules( + adata, + label_color_dict=color_dict, + color_map_name=color_map_name, + overwrite=overwrite + ) + logger.info(f"{adata.uns[f'{color_map_name}_summary']}") + + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + if "analysis" in params["outputs"]: + results_dict["analysis"] = adata + + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + logger.info("Append Pin Color Rule analysis completed successfully.") + return saved_files + else: + # Return the adata object directly for in-memory workflows + logger.info("Returning AnnData object for in-memory use") + return adata + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) < 2: + print( + "Usage: python add_pin_color_rule_template.py " + "[output_dir]", + file=sys.stderr + ) + sys.exit(1) + + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + + if isinstance(result, dict): + print("\nOutput files:") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") + else: + print("\nReturned AnnData object") diff --git a/src/spac/templates/arcsinh_normalization_template.py b/src/spac/templates/arcsinh_normalization_template.py index 7a7418f6..fcdf62da 100644 --- a/src/spac/templates/arcsinh_normalization_template.py +++ b/src/spac/templates/arcsinh_normalization_template.py @@ -2,6 +2,9 @@ Platform-agnostic Arcsinh Normalization template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Follows standardized output schema where analysis is saved as a file. + Usage ----- >>> from spac.templates.arcsinh_normalization_template import run_from_json @@ -12,6 +15,7 @@ from pathlib import Path from typing import Any, Dict, Union import pandas as pd +import logging # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) @@ -19,7 +23,7 @@ from spac.transformations import arcsinh_transformation from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, text_to_value, ) @@ -27,7 +31,8 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], Any]: """ Execute Arcsinh Normalization analysis with parameters from JSON. @@ -36,20 +41,69 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the adata object - directly for in-memory workflows. Default is True. + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Table_to_Process": "Original", + "Co_Factor": "5.0", + "Percentile": "None", + "Output_Table_Name": "arcsinh", + "Per_Batch": "False", + "Annotation": "None", + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If True, saves the AnnData object + to a pickle file. If False, returns the AnnData object directly + for in-memory workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. All outputs will be saved relative to this directory. Returns ------- dict or AnnData - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed AnnData object + If save_to_disk=True: Dictionary of saved file paths with structure: + {"analysis": "path/to/output.pickle"} + If save_to_disk=False: The processed AnnData object for in-memory use + + Notes + ----- + Output Structure: + - Analysis output is saved as a single pickle file (standardized for analysis outputs) + - When save_to_disk=False, the AnnData object is returned for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["analysis"]) # Path to saved pickle file + >>> # './output.pickle' + + >>> # Get results in memory for further processing + >>> adata = run_from_json("params.json", save_to_disk=False) + >>> # Can now work with adata object directly + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") """ # Parse parameters from JSON params = parse_params(json_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + # Analysis uses file type per standardized schema + if "outputs" not in params: + params["outputs"] = { + "analysis": {"type": "file", "name": "output.pickle"} + } + # Load the upstream analysis data adata = load_input(params["Upstream_Analysis"]) @@ -98,44 +152,67 @@ def run_from_json( output_layer=output_layer, per_batch=per_batch, annotation=annotation - ) + ) - print(f"Transformed data stored in layer: {output_layer}") + logging.info(f"Transformed data stored in layer: {output_layer}") dataframe = pd.DataFrame(transformed_data.layers[output_layer]) - print(dataframe.describe()) + logging.info(f"Arcsinh transformation summary:\n{dataframe.describe()}") - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "transform_output.pickle") - # Default to pickle format if no recognized extension - if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): - output_file = output_file + '.pickle' + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} - saved_files = save_outputs({output_file: transformed_data}) + if "analysis" in params["outputs"]: + results_dict["analysis"] = transformed_data - print(f"Arcsinh Normalization completed → {saved_files[output_file]}") + # Use centralized save_results function + # All file handling and logging is now done by save_results + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + logging.info( + f"Arcsinh Normalization completed → {saved_files['analysis']}" + ) return saved_files else: # Return the adata object directly for in-memory workflows - print("Returning AnnData object (not saving to file)") + logging.info("Returning AnnData object (not saving to file)") return transformed_data # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python arcsinh_normalization_template.py ", + "Usage: python arcsinh_normalization_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + # Display results based on return type if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, path in result.items(): + print(f" {key}: {path}") else: - print("\nReturned AnnData object") \ No newline at end of file + print("\nReturned AnnData object for in-memory use") + print(f"AnnData: {result}") diff --git a/src/spac/templates/binary_to_categorical_annotation_template.py b/src/spac/templates/binary_to_categorical_annotation_template.py index 3cb82b69..127a8e4a 100644 --- a/src/spac/templates/binary_to_categorical_annotation_template.py +++ b/src/spac/templates/binary_to_categorical_annotation_template.py @@ -2,6 +2,9 @@ Platform-agnostic Binary to Categorical Annotation template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.binary_to_categorical_annotation_template import \ @@ -10,10 +13,10 @@ """ import json import sys -import pickle from pathlib import Path from typing import Any, Dict, Union import pandas as pd +import logging # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) @@ -21,14 +24,15 @@ from spac.data_utils import bin2cat from spac.utils import check_column_name from spac.templates.template_utils import ( - save_outputs, + save_results, parse_params, ) def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], pd.DataFrame]: """ Execute Binary to Categorical Annotation analysis with parameters from @@ -37,35 +41,80 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the dataframe + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Dataset": "path/to/dataframe.csv", + "Binary_Annotation_Columns": ["Col1", "Col2", "Col3"], + "New_Annotation_Name": "cell_labels", + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If True, saves the DataFrame with + converted annotations to a CSV file. If False, returns the DataFrame directly for in-memory workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. All outputs will be saved relative to this directory. Returns ------- dict or DataFrame - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed DataFrame + If save_to_disk=True: Dictionary of saved file paths with structure: + { + "dataframe": "path/to/dataframe.csv" + } + If save_to_disk=False: The processed DataFrame with categorical annotation + + Notes + ----- + Output Structure: + - DataFrame is saved as a single CSV file + - When save_to_disk=False, the DataFrame is returned for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["dataframe"]) # Path to saved CSV file + + >>> # Get results in memory + >>> converted_df = run_from_json("params.json", save_to_disk=False) + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") """ # Parse parameters from JSON params = parse_params(json_path) - # Load upstream data - could be DataFrame, CSV, or pickle + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + if "outputs" not in params: + params["outputs"] = { + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + + # Load upstream data - DataFrame or CSV file upstream_dataset = params["Upstream_Dataset"] if isinstance(upstream_dataset, pd.DataFrame): - input_dataset = upstream_dataset # Direct DataFrame from prev step + input_dataset = upstream_dataset # Direct DataFrame from previous step elif isinstance(upstream_dataset, (str, Path)): + # Galaxy passes .dat files, but they contain CSV data + # Don't check extension - directly read as CSV path = Path(upstream_dataset) - if path.suffix.lower() == '.csv': + try: input_dataset = pd.read_csv(path) - elif path.suffix.lower() in ['.pickle', '.pkl', '.p']: - with open(path, 'rb') as f: - input_dataset = pickle.load(f) - else: + logging.info(f"Successfully loaded CSV data from: {path}") + except Exception as e: raise ValueError( - f"Unsupported file format: {path.suffix}. " - f"Supported formats: .csv, .pickle, .pkl, .p" + f"Failed to read CSV data from '{path}'. " + f"This tool expects CSV/tabular format. " + f"Error: {str(e)}" ) else: raise TypeError( @@ -88,43 +137,67 @@ def run_from_json( new_annotation=new_annotation ) - print(converted_df.info()) + logging.info(converted_df.info()) - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get( - "Output_File", "converted_annotations.csv" - ) + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} - saved_files = save_outputs({output_file: converted_df}) + # Check for dataframe output + if "dataframe" in params["outputs"]: + results_dict["dataframe"] = converted_df - print( - f"Binary to Categorical Annotation completed → " - f"{saved_files[output_file]}" + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir ) + + logging.info("Binary to Categorical Annotation completed successfully.") return saved_files else: - # Return the dataframe directly for in-memory workflows - print("Returning DataFrame (not saving to file)") + # Return the DataFrame directly for in-memory workflows + logging.info("Returning DataFrame for in-memory use") return converted_df # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( "Usage: python binary_to_categorical_annotation_template.py " - "", + " [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + # Display results based on return type if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nReturned DataFrame") \ No newline at end of file + print("\nReturned DataFrame") + print(f"DataFrame shape: {result.shape}") diff --git a/src/spac/templates/boxplot_template.py b/src/spac/templates/boxplot_template.py index 6207c8b4..791e5e5e 100644 --- a/src/spac/templates/boxplot_template.py +++ b/src/spac/templates/boxplot_template.py @@ -2,6 +2,9 @@ Platform-agnostic Boxplot template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Follows standardized output schema where figures are saved as directories. + Usage ----- >>> from spac.templates.boxplot_template import run_from_json @@ -22,7 +25,7 @@ from spac.visualization import boxplot from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, text_to_value, ) @@ -30,9 +33,10 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True, - show_plot: bool = True -) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: + save_to_disk: bool = True, + show_plot: bool = True, + output_dir: str = None, +) -> Union[Dict[str, Union[str, List[str]]], Tuple[Any, pd.DataFrame]]: """ Execute Boxplot analysis with parameters from JSON. Replicates the NIDAP template functionality exactly. @@ -40,22 +44,73 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the figure and + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Primary_Annotation": "cell_type", + "Feature_s_to_Plot": ["CD4", "CD8"], + "outputs": { + "figures": {"type": "directory", "name": "figures"}, + "dataframe": {"type": "file", "name": "output.csv"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If True, saves figures to a directory + and summary statistics to a CSV file. If False, returns the figure and summary dataframe directly for in-memory workflows. Default is True. show_plot : bool, optional Whether to display the plot. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. All outputs will be saved relative to this directory. Returns ------- dict or tuple - If save_results=True: Dictionary of saved file paths - If save_results=False: Tuple of (figure, summary_dataframe) + If save_to_disk=True: Dictionary of saved file paths with structure: + { + "figures": ["path/to/figures/boxplot.png"], # List of figure paths + "DataFrame": "path/to/output.csv" # Single file path + } + If save_to_disk=False: Tuple of (matplotlib.figure.Figure, pd.DataFrame) + containing the figure object and summary statistics dataframe + + Notes + ----- + Output Structure: + - Figures are saved in a directory (standardized for all figure outputs) + - Summary statistics are saved as a single CSV file + - When save_to_disk=False, objects are returned for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["figure"]) # List of paths to saved plots + >>> # ['./figures/boxplot.png'] + + >>> # Get results in memory + >>> fig, summary_df = run_from_json("params.json", save_to_disk=False) + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") """ # Parse parameters from JSON params = parse_params(json_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + # Figures use directory type per standardized schema + if "outputs" not in params: + params["outputs"] = { + "figures": {"type": "directory", "name": "figures"}, + "dataframe": {"type": "file", "name": "output.csv"} + } + # Load the upstream analysis data adata = load_input(params["Upstream_Analysis"]) @@ -66,7 +121,7 @@ def run_from_json( feature_to_plot = params.get("Feature_s_to_Plot", ["All"]) log_scale = params.get("Value_Axis_Log_Scale", False) - # Figure parameters + # Extract figure parameters with defaults figure_title = params.get("Figure_Title", "BoxPlot") figure_horizontal = params.get("Horizontal_Plot", False) fig_width = params.get("Figure_Width", 12) @@ -75,21 +130,21 @@ def run_from_json( font_size = params.get("Font_Size", 10) showfliers = params.get("Keep_Outliers", True) - # Process parameters exactly as in NIDAP template - if layer_to_plot == "Original": - layer_to_plot = None - - if second_annotation == "None": - second_annotation = None - - if annotation == "None": - annotation = None + # Process parameters to match expected format + # Convert "None" strings to actual None values + layer_to_plot = None if layer_to_plot == "Original" else layer_to_plot + second_annotation = None if second_annotation == "None" else second_annotation + annotation = None if annotation == "None" else annotation - if figure_horizontal: - figure_orientation = "h" - else: - figure_orientation = "v" + # Convert horizontal flag to orientation string + figure_orientation = "h" if figure_horizontal else "v" + + # Handle feature selection + if isinstance(feature_to_plot, str): + # Convert single string to list + feature_to_plot = [feature_to_plot] + # Check for "All" features selection if any(item == "All" for item in feature_to_plot): logging.info("Plotting All Features") feature_to_plot = adata.var_names.tolist() @@ -133,49 +188,82 @@ def run_from_json( except Exception as e: logging.debug("Legend does not exist.") + # Apply tight layout to prevent label cutoff plt.tight_layout() if show_plot: plt.show() - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "boxplot_summary.csv") - saved_files = save_outputs({output_file: summary_df}) - - # Also save the figure if specified - figure_file = params.get("Figure_File", None) - if figure_file: - saved_files.update(save_outputs({figure_file: fig})) - - logging.info(f"Boxplot completed → {saved_files[output_file]}") + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + # Package figure in a dictionary for directory saving + # This ensures it's saved in a directory per standardized schema + if "figures" in params["outputs"]: + results_dict["figures"] = {"boxplot": fig} # Dict triggers directory save + + # Check for DataFrames output (case-insensitive) + if any(k.lower() == "dataframe" for k in params["outputs"].keys()): + results_dict["dataframe"] = summary_df + + # Use centralized save_results function + # All file handling and logging is now done by save_results + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + logging.info("Boxplot analysis completed successfully.") return saved_files else: - # Return the figure and summary dataframe for in-memory workflows + # Return objects directly for in-memory workflows logging.info( - "Returning figure and summary dataframe (not saving to file)" + "Returning figure and summary dataframe for in-memory use" ) return fig, summary_df # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python boxplot_template.py ", + "Usage: python boxplot_template.py [output_dir]", file=sys.stderr ) sys.exit(1) # Set up logging for CLI usage - logging.basicConfig(level=logging.INFO) + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) - result = run_from_json(sys.argv[1]) + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + # Display results based on return type if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nReturned figure and summary dataframe") \ No newline at end of file + fig, summary_df = result + print("\nReturned figure and summary dataframe for in-memory use") + print(f"Figure size: {fig.get_size_inches()}") + print(f"Summary shape: {summary_df.shape}") + print("\nSummary statistics preview:") + print(summary_df.head()) \ No newline at end of file diff --git a/src/spac/templates/calculate_centroid_template.py b/src/spac/templates/calculate_centroid_template.py index e6f56658..b59add49 100644 --- a/src/spac/templates/calculate_centroid_template.py +++ b/src/spac/templates/calculate_centroid_template.py @@ -2,6 +2,9 @@ Platform-agnostic Calculate Centroid template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.calculate_centroid_template import run_from_json @@ -10,9 +13,9 @@ import json import sys from pathlib import Path -from typing import Any, Dict, Union +from typing import Any, Dict, Union, Tuple import pandas as pd -import pickle +import logging # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) @@ -20,14 +23,15 @@ from spac.data_utils import calculate_centroid from spac.utils import check_column_name from spac.templates.template_utils import ( - save_outputs, + save_results, parse_params, ) def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], pd.DataFrame]: """ Execute Calculate Centroid analysis with parameters from JSON. @@ -36,35 +40,83 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the DataFrame + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Dataset": "path/to/dataframe.csv", + "Min_X_Coordinate_Column_Name": "XMin", + "Max_X_Coordinate_Column_Name": "XMax", + "Min_Y_Coordinate_Column_Name": "YMin", + "Max_Y_Coordinate_Column_Name": "YMax", + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If True, saves the DataFrame with + calculated centroids to a CSV file. If False, returns the DataFrame directly for in-memory workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. All outputs will be saved relative to this directory. Returns ------- dict or DataFrame - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed DataFrame + If save_to_disk=True: Dictionary of saved file paths with structure: + { + "dataframe": "path/to/dataframe.csv" + } + If save_to_disk=False: The processed DataFrame with centroids + + Notes + ----- + Output Structure: + - DataFrame is saved as a single CSV file + - When save_to_disk=False, the DataFrame is returned for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["dataframe"]) # Path to saved CSV file + + >>> # Get results in memory + >>> centroid_df = run_from_json("params.json", save_to_disk=False) + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") """ # Parse parameters from JSON params = parse_params(json_path) - # Load upstream data - could be DataFrame, CSV, or pickle + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + # DataFrames typically use file type per standardized schema + if "outputs" not in params: + params["outputs"] = { + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + + # Load upstream data - DataFrame or CSV file upstream_dataset = params["Upstream_Dataset"] if isinstance(upstream_dataset, pd.DataFrame): input_dataset = upstream_dataset # Direct DataFrame from previous step elif isinstance(upstream_dataset, (str, Path)): + # Galaxy passes .dat files, but they contain CSV data + # Don't check extension - directly read as CSV path = Path(upstream_dataset) - if path.suffix.lower() == '.csv': + try: input_dataset = pd.read_csv(path) - elif path.suffix.lower() in ['.pickle', '.pkl', '.p']: - with open(path, 'rb') as f: - input_dataset = pickle.load(f) - else: + logging.info(f"Successfully loaded CSV data from: {path}") + except Exception as e: raise ValueError( - f"Unsupported file format: {path.suffix}. " - f"Supported formats: .csv, .pickle, .pkl, .p" + f"Failed to read CSV data from '{path}'. " + f"This tool expects CSV/tabular format. " + f"Error: {str(e)}" ) else: raise TypeError( @@ -93,40 +145,67 @@ def run_from_json( new_y=new_y ) - print(centroid_calculated.info()) + logging.info(centroid_calculated.info()) - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "centroid_calculated.csv") - # Default to CSV format if no recognized extension - if not output_file.endswith(('.csv', '.pickle', '.pkl')): - output_file = output_file + '.csv' + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + # Check for dataframe output + if "dataframe" in params["outputs"]: + results_dict["dataframe"] = centroid_calculated - saved_files = save_outputs({output_file: centroid_calculated}) + # Use centralized save_results function + # All file handling and logging is now done by save_results + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) - print(f"Calculate Centroid completed → {saved_files[output_file]}") + logging.info("Calculate Centroid analysis completed successfully.") return saved_files else: # Return the DataFrame directly for in-memory workflows - print("Returning DataFrame (not saving to file)") + logging.info("Returning DataFrame for in-memory use") return centroid_calculated # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python calculate_centroid_template.py ", + "Usage: python calculate_centroid_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + # Display results based on return type if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nReturned DataFrame") \ No newline at end of file + print("\nReturned DataFrame") + print(f"DataFrame shape: {result.shape}") diff --git a/src/spac/templates/combine_annotations_template.py b/src/spac/templates/combine_annotations_template.py index b6a8b3c0..b152978b 100644 --- a/src/spac/templates/combine_annotations_template.py +++ b/src/spac/templates/combine_annotations_template.py @@ -2,6 +2,9 @@ Platform-agnostic Combine Annotations template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.combine_annotations_template import run_from_json @@ -9,6 +12,7 @@ """ import json import sys +import logging from pathlib import Path from typing import Any, Dict, Union, List @@ -18,14 +22,17 @@ from spac.data_utils import combine_annotations from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, ) +logger = logging.getLogger(__name__) + def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], Any]: """ Execute Combine Annotations analysis with parameters from JSON. @@ -34,20 +41,56 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the adata object + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Annotations_Names": ["annotation1", "annotation2"], + "New_Annotation_Name": "combined_annotation", + "Separator": "_", + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"}, + "analysis": {"type": "file", "name": "output.pickle"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If False, returns the adata object directly for in-memory workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. Returns ------- dict or AnnData - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed AnnData object + If save_to_disk=True: Dictionary of saved file paths with structure: + { + "dataframe": "path/to/dataframe.csv", + "analysis": "path/to/output.pickle" + } + If save_to_disk=False: The processed AnnData object + + Notes + ----- + Output Structure: + - Analysis output is saved as a pickle file + - DataFrame (label counts) is saved as a CSV file + - When save_to_disk=False, the AnnData object is returned for programmatic use """ # Parse parameters from JSON params = parse_params(json_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + if "outputs" not in params: + params["outputs"] = { + "dataframe": {"type": "file", "name": "dataframe.csv"}, + "analysis": {"type": "file", "name": "output.pickle"} + } + # Load the upstream analysis data adata = load_input(params["Upstream_Analysis"]) @@ -63,57 +106,76 @@ def run_from_json( new_annotation_name=new_annotation ) - print("After combining annotations: \n", adata) + logger.info(f"After combining annotations: \n{adata}") value_counts = adata.obs[new_annotation].value_counts(dropna=False) - print(f"Unique labels in {new_annotation}") - print(value_counts) + logger.info(f"Unique labels in {new_annotation}") + logger.info(f"{value_counts}") - # create the frequency CSV for download + # Create the frequency CSV for download df_counts = ( value_counts .rename_axis(new_annotation) # move index to a column name .reset_index(name='count') # two columns: label | count ) - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "transform_output.pickle") - # Default to pickle format if no recognized extension - if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): - output_file = output_file + '.pickle' - - # Also save the counts CSV - csv_name = f"{new_annotation}_counts.csv" - - saved_files = save_outputs({ - output_file: adata, - csv_name: df_counts - }) - - print(f"\nLabel-count table written to {csv_name}") - print(f"Combine Annotations completed → {saved_files[output_file]}") + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + if "dataframe" in params["outputs"]: + results_dict["dataframe"] = df_counts + + if "analysis" in params["outputs"]: + results_dict["analysis"] = adata + + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + logger.info("Combine Annotations analysis completed successfully.") return saved_files else: # Return the adata object directly for in-memory workflows - print("Returning AnnData object (not saving to file)") + logger.info("Returning AnnData object for in-memory use") return adata # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python combine_annotations_template.py ", + "Usage: python combine_annotations_template.py " + "[output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nReturned data object") \ No newline at end of file + print("\nReturned AnnData object") diff --git a/src/spac/templates/combine_dataframes_template.py b/src/spac/templates/combine_dataframes_template.py index 142d6517..6d23bbc4 100644 --- a/src/spac/templates/combine_dataframes_template.py +++ b/src/spac/templates/combine_dataframes_template.py @@ -2,6 +2,9 @@ Platform-agnostic Combine DataFrames template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.combine_dataframes_template import run_from_json @@ -12,22 +15,22 @@ from pathlib import Path from typing import Any, Dict, Union import pandas as pd -import pickle +import logging # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) -# Import SPAC functions from NIDAP template from spac.data_utils import combine_dfs from spac.templates.template_utils import ( - save_outputs, + save_results, parse_params, ) def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], pd.DataFrame]: """ Execute Combine DataFrames analysis with parameters from JSON. @@ -36,35 +39,79 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the dataframe - directly for in-memory workflows. Default is True. + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "First_Dataframe": "path/to/first.csv", + "Second_Dataframe": "path/to/second.csv", + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If True, saves the combined DataFrame + to a CSV file. If False, returns the DataFrame directly for in-memory + workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. All outputs will be saved relative to this directory. Returns ------- dict or DataFrame - If save_results=True: Dictionary of saved file paths - If save_results=False: The combined DataFrame + If save_to_disk=True: Dictionary of saved file paths with structure: + { + "dataframe": "path/to/dataframe.csv" + } + If save_to_disk=False: The combined DataFrame + + Notes + ----- + Output Structure: + - DataFrame is saved as a single CSV file + - When save_to_disk=False, the DataFrame is returned for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["dataframe"]) # Path to saved CSV file + + >>> # Get results in memory + >>> combined_df = run_from_json("params.json", save_to_disk=False) + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") """ # Parse parameters from JSON params = parse_params(json_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + if "outputs" not in params: + params["outputs"] = { + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + # Load the first dataframe dataset_A = params["First_Dataframe"] if isinstance(dataset_A, pd.DataFrame): dataset_A = dataset_A # Direct DataFrame from previous step elif isinstance(dataset_A, (str, Path)): + # Galaxy passes .dat files, but they contain CSV data + # Don't check extension - directly read as CSV path = Path(dataset_A) - if path.suffix.lower() == '.csv': + try: dataset_A = pd.read_csv(path) - elif path.suffix.lower() in ['.pickle', '.pkl', '.p']: - with open(path, 'rb') as f: - dataset_A = pickle.load(f) - else: + logging.info(f"Successfully loaded first DataFrame from: {path}") + except Exception as e: raise ValueError( - f"Unsupported file format: {path.suffix}. " - f"Supported formats: .csv, .pickle, .pkl, .p" + f"Failed to read CSV data from '{path}'. " + f"This tool expects CSV/tabular format. " + f"Error: {str(e)}" ) else: raise TypeError( @@ -77,16 +124,17 @@ def run_from_json( if isinstance(dataset_B, pd.DataFrame): dataset_B = dataset_B # Direct DataFrame from previous step elif isinstance(dataset_B, (str, Path)): + # Galaxy passes .dat files, but they contain CSV data + # Don't check extension - directly read as CSV path = Path(dataset_B) - if path.suffix.lower() == '.csv': + try: dataset_B = pd.read_csv(path) - elif path.suffix.lower() in ['.pickle', '.pkl', '.p']: - with open(path, 'rb') as f: - dataset_B = pickle.load(f) - else: + logging.info(f"Successfully loaded second DataFrame from: {path}") + except Exception as e: raise ValueError( - f"Unsupported file format: {path.suffix}. " - f"Supported formats: .csv, .pickle, .pkl, .p" + f"Failed to read CSV data from '{path}'. " + f"This tool expects CSV/tabular format. " + f"Error: {str(e)}" ) else: raise TypeError( @@ -97,43 +145,73 @@ def run_from_json( # Extract parameters input_df_lists = [dataset_A, dataset_B] - print("Information about the first dataset:") - print(dataset_A.info()) - print("\n\nInformation about the second dataset:") - print(dataset_B.info()) + logging.info("Information about the first dataset:") + logging.info(dataset_A.info()) + logging.info("\n\nInformation about the second dataset:") + logging.info(dataset_B.info()) combined_dfs = combine_dfs(input_df_lists) - print("\n\nInformation about the combined dataset:") - print(combined_dfs.info()) - - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "combined_dataframes.csv") - saved_files = save_outputs({output_file: combined_dfs}) + logging.info("\n\nInformation about the combined dataset:") + logging.info(combined_dfs.info()) + + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + # Check for dataframe output + if "dataframe" in params["outputs"]: + results_dict["dataframe"] = combined_dfs - print(f"Combine DataFrames completed → {saved_files[output_file]}") + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + logging.info("Combine DataFrames completed successfully.") return saved_files else: - # Return the dataframe directly for in-memory workflows - print("Returning combined DataFrame (not saving to file)") + # Return the DataFrame directly for in-memory workflows + logging.info("Returning combined DataFrame for in-memory use") return combined_dfs # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python combine_dataframes_template.py ", + "Usage: python combine_dataframes_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) - + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + + # Display results based on return type if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nReturned combined DataFrame") \ No newline at end of file + print("\nReturned combined DataFrame") + print(f"DataFrame shape: {result.shape}") diff --git a/src/spac/templates/downsample_cells_template.py b/src/spac/templates/downsample_cells_template.py index 90d36136..761135e0 100644 --- a/src/spac/templates/downsample_cells_template.py +++ b/src/spac/templates/downsample_cells_template.py @@ -2,6 +2,9 @@ Platform-agnostic Downsample Cells template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.downsample_cells_template import run_from_json @@ -10,9 +13,9 @@ import json import sys from pathlib import Path -from typing import Any, Dict, Union +from typing import Any, Dict, Union, Tuple import pandas as pd -import pickle +import logging # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) @@ -20,7 +23,7 @@ from spac.data_utils import downsample_cells from spac.utils import check_column_name from spac.templates.template_utils import ( - save_outputs, + save_results, parse_params, text_to_value, ) @@ -28,7 +31,8 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], pd.DataFrame]: """ Execute Downsample Cells analysis with parameters from JSON. @@ -37,36 +41,76 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the dataframe - directly for in-memory workflows. Default is True. + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Dataset": "path/to/dataframe.csv", + "Annotations_List": ["cell_type", "tissue"], + "Number_of_Samples": 1000, + "Stratify_Option": true, + "Random_Selection": true, + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If True, saves the downsampled DataFrame + to a CSV file. If False, returns the DataFrame directly for in-memory + workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. All outputs will be saved relative to this directory. Returns ------- dict or DataFrame - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed DataFrame + If save_to_disk=True: Dictionary of saved file paths with structure: + { + "dataframe": "path/to/dataframe.csv" + } + If save_to_disk=False: The downsampled DataFrame + + Notes + ----- + Output Structure: + - DataFrame is saved as a single CSV file + - When save_to_disk=False, the DataFrame is returned for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["dataframe"]) # Path to saved CSV file + + >>> # Get results in memory + >>> downsampled_df = run_from_json("params.json", save_to_disk=False) + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") """ # Parse parameters from JSON params = parse_params(json_path) - # Load upstream data - could be DataFrame, CSV, or pickle + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + # DataFrames typically use file type per standardized schema + if "outputs" not in params: + params["outputs"] = { + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + + # Load upstream data - could be DataFrame, CSV upstream_dataset = params["Upstream_Dataset"] if isinstance(upstream_dataset, pd.DataFrame): input_dataset = upstream_dataset # Direct DF from previous step elif isinstance(upstream_dataset, (str, Path)): - path = Path(upstream_dataset) - if path.suffix.lower() == '.csv': - input_dataset = pd.read_csv(path) - elif path.suffix.lower() in ['.pickle', '.pkl', '.p']: - with open(path, 'rb') as f: - input_dataset = pickle.load(f) - else: - raise ValueError( - f"Unsupported file format: {path.suffix}. " - f"Supported formats: .csv, .pickle, .pkl, .p" - ) + try: + input_dataset = pd.read_csv(upstream_dataset) + except Exception as e: + raise ValueError(f"Failed to read CSV from {upstream_dataset}: {e}") else: raise TypeError( f"Upstream_Dataset must be DataFrame or file path. " @@ -97,43 +141,68 @@ def run_from_json( min_threshold=min_threshold ) - print("Downsampled! Processed dataset info:") - print(down_sampled_dataset.info()) + logging.info("Downsampled! Processed dataset info:") + logging.info(down_sampled_dataset.info()) - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "downsampled_data.csv") - # Default to CSV format if no recognized extension - if not output_file.endswith(('.csv', '.pickle', '.pkl')): - output_file = output_file + '.csv' + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} - saved_files = save_outputs({output_file: down_sampled_dataset}) + # Check for dataframe output + if "dataframe" in params["outputs"]: + results_dict["dataframe"] = down_sampled_dataset - print( - f"Downsample Cells completed → {saved_files[output_file]}" + # Use centralized save_results function + # All file handling and logging is now done by save_results + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir ) + + logging.info("Downsample Cells analysis completed successfully.") return saved_files else: # Return the dataframe directly for in-memory workflows - print("Returning DataFrame (not saving to file)") + logging.info("Returning DataFrame for in-memory use") return down_sampled_dataset # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python downsample_cells_template.py ", + "Usage: python downsample_cells_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + # Display results based on return type if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nReturned DataFrame") \ No newline at end of file + print("\nReturned DataFrame") + print(f"DataFrame shape: {result.shape}") diff --git a/src/spac/templates/hierarchical_heatmap_template.py b/src/spac/templates/hierarchical_heatmap_template.py index 8bee4958..92bec6cc 100644 --- a/src/spac/templates/hierarchical_heatmap_template.py +++ b/src/spac/templates/hierarchical_heatmap_template.py @@ -21,7 +21,7 @@ from spac.utils import check_feature from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, text_to_value, ) @@ -29,8 +29,9 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True, - show_plot: bool = True + save_results_flag: bool = True, + show_plot: bool = True, + output_dir: Union[str, Path] = None ) -> Union[Dict[str, str], pd.DataFrame]: """ Execute Hierarchical Heatmap analysis with parameters from JSON. @@ -40,17 +41,19 @@ def run_from_json( ---------- json_path : str, Path, or dict Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional + save_results_flag : bool, optional Whether to save results to file. If False, returns the figure and dataframe directly for in-memory workflows. Default is True. show_plot : bool, optional Whether to display the plot. Default is True. + output_dir : str or Path, optional + Directory for outputs. If None, uses params['Output_Directory'] or '.' Returns ------- dict or DataFrame - If save_results=True: Dictionary of saved file paths - If save_results=False: The mean intensity dataframe + If save_results_flag=True: Dictionary of saved file paths + If save_results_flag=False: The mean intensity dataframe """ # Parse parameters from JSON params = parse_params(json_path) @@ -158,37 +161,55 @@ def run_from_json( if show_plot: plt.show() - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "plots.csv") - saved_files = save_outputs({output_file: mean_intensity}) - - print( - f"Hierarchical Heatmap completed → " - f"{saved_files[output_file]}" + # Handle results based on save_results_flag + if save_results_flag: + # Prepare results dictionary based on outputs config + results_dict = {} + + # Package figure in a dictionary for directory saving + # This ensures it's saved in a directory per standardized schema + if "figures" in params.get("outputs", {}): + results_dict["figures"] = {"hierarchical_heatmap": clustergrid.fig} + + # Check for dataframe output + if "dataframe" in params.get("outputs", {}): + results_dict["dataframe"] = mean_intensity + + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir ) + + print("Hierarchical Heatmap completed successfully.") return saved_files else: - # Return the figure and dataframe directly for in-memory workflows - print("Returning figure and dataframe (not saving to file)") + # Return the dataframe directly for in-memory workflows + print("Returning mean intensity dataframe (not saving to file)") return mean_intensity # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python hierarchical_heatmap_template.py ", + "Usage: python hierarchical_heatmap_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + result = run_from_json(sys.argv[1], output_dir=output_dir) if isinstance(result, dict): print("\nOutput files:") for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + if isinstance(filepath, list): + print(f" {filename}: {len(filepath)} files in directory") + else: + print(f" {filename}: {filepath}") else: - print("\nReturned figure and dataframe") \ No newline at end of file + print("\nReturned mean intensity dataframe") diff --git a/src/spac/templates/histogram_template.py b/src/spac/templates/histogram_template.py index 997322a3..0a3924d4 100644 --- a/src/spac/templates/histogram_template.py +++ b/src/spac/templates/histogram_template.py @@ -2,6 +2,9 @@ Platform-agnostic Histogram template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.histogram_template import run_from_json @@ -10,11 +13,11 @@ import json import sys from pathlib import Path -from typing import Any, Dict, Union, Optional, Tuple +from typing import Any, Dict, Union, Optional, Tuple, List import pandas as pd import matplotlib.pyplot as plt import seaborn as sns -import warnings +import logging # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) @@ -22,7 +25,7 @@ from spac.visualization import histogram from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, text_to_value, ) @@ -30,9 +33,10 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True, - show_plot: bool = False -) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: + save_to_disk: bool = True, + show_plot: bool = False, + output_dir: str = None, +) -> Union[Dict[str, Union[str, List[str]]], Tuple[Any, pd.DataFrame]]: """ Execute Histogram analysis with parameters from JSON. Replicates the NIDAP template functionality exactly. @@ -40,22 +44,51 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the figure and + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Plot_By": "Annotation", + "Annotation": "cell_type", + ... + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"}, + "figures": {"type": "directory", "name": "figures_dir"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If False, returns the figure and dataframe directly for in-memory workflows. Default is True. show_plot : bool, optional Whether to display the plot. Default is False. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. Returns ------- dict or tuple - If save_results=True: Dictionary of saved file paths - If save_results=False: Tuple of (figure, dataframe) + If save_to_disk=True: Dictionary of saved file paths + If save_to_disk=False: Tuple of (figure, dataframe) """ + # Set up logging + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + # Parse parameters from JSON params = parse_params(json_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + if "outputs" not in params: + params["outputs"] = { + "dataframe": {"type": "file", "name": "dataframe.csv"}, + "figures": {"type": "directory", "name": "figures_dir"} + } + # Load the upstream analysis data adata = load_input(params["Upstream_Analysis"]) @@ -98,7 +131,7 @@ def run_from_json( if histplot_by == "Annotation": if adata.obs.columns.size > 0: annotation = adata.obs.columns[0] - print( + logger.info( f'No annotation specified. Using the first annotation ' f'"{annotation}" as default.' ) @@ -109,7 +142,7 @@ def run_from_json( else: if adata.var_names.size > 0: feature = adata.var_names[0] - print( + logger.info( f'No feature specified. Using the first feature ' f'"{feature}" as default.' ) @@ -136,14 +169,14 @@ def run_from_json( elif annotation is not None: if take_X_log: take_X_log = False - print( - "Warning: Take X log should only apply to feature. " + logger.warning( + "Take X log should only apply to feature. " "Setting Take X Log to False." ) if bins != 'auto': bins = 'auto' - print( - "Warning: Bin number should only apply to feature. " + logger.warning( + "Bin number should only apply to feature. " "Setting bin number calculation to auto." ) @@ -200,12 +233,12 @@ def run_from_json( for num in new_fig_nums: if num != histogram_fig_num: plt.close(plt.figure(num)) - print(f"Closed extra figure {num}") + logger.debug(f"Closed extra figure {num}") # Process each axis for ax in axes: if feature: - print(f'Plotting Feature: "{feature}"') + logger.info(f'Plotting Feature: "{feature}"') if ax.get_legend() is not None: if legend_in_figure: sns.move_legend(ax, legend_location) @@ -230,8 +263,8 @@ def run_from_json( text_to_value(group_by) ].dropna().unique() if len(axes) != len(unique_groups): - print( - "Warning: Number of axes does not match number of " + logger.warning( + "Number of axes does not match number of " "groups. Titles may not correspond correctly." ) for ax, grp in zip(axes, unique_groups): @@ -244,39 +277,73 @@ def run_from_json( plt.tight_layout() - print("Displaying top 10 rows of histogram dataframe:") + logger.info("Displaying top 10 rows of histogram dataframe:") print(df_counts.head(10)) if show_plot: plt.show() - plt.close('all') + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + # Check for dataframe output + if "dataframe" in params["outputs"]: + results_dict["dataframe"] = df_counts - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "plots.csv") - saved_files = save_outputs({output_file: df_counts}) + # Check for figures output + if "figures" in params["outputs"]: + results_dict["figures"] = {"histogram": fig} + + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) - print(f"Histogram completed → {saved_files[output_file]}") + plt.close('all') + + logger.info("Histogram analysis completed successfully.") return saved_files else: # Return the figure and dataframe directly for in-memory workflows - print("Returning figure and dataframe (not saving to file)") + logger.info("Returning figure and dataframe for in-memory use") return fig, df_counts # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: - print("Usage: python histogram_template.py ") + if len(sys.argv) < 2: + print( + "Usage: python histogram_template.py [output_dir]", + file=sys.stderr + ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nReturned figure and dataframe") \ No newline at end of file + print("\nReturned figure and dataframe") diff --git a/src/spac/templates/interactive_spatial_plot_template.py b/src/spac/templates/interactive_spatial_plot_template.py index eed1b014..e63e0df2 100644 --- a/src/spac/templates/interactive_spatial_plot_template.py +++ b/src/spac/templates/interactive_spatial_plot_template.py @@ -2,6 +2,9 @@ Platform-agnostic Interactive Spatial Plot template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Follows standardized output schema where HTML files are saved as a directory. + Usage ----- >>> from spac.templates.interactive_spatial_plot_template import run_from_json @@ -21,7 +24,7 @@ from spac.visualization import interactive_spatial_plot from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, text_to_value, ) @@ -29,8 +32,9 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True -) -> Optional[Dict[str, str]]: + save_to_disk: bool = True, + output_dir: str = None, +) -> Union[Dict[str, Union[str, List[str]]], None]: """ Execute Interactive Spatial Plot analysis with parameters from JSON. Replicates the NIDAP template functionality exactly. @@ -38,22 +42,67 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. Default is True. + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Color_By": "Annotation", + "Annotation_s_to_Highlight": ["renamed_phenotypes"], + "outputs": { + "html": {"type": "directory", "name": "html_dir"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If False, returns None as plots are + shown interactively. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. All outputs will be saved relative to this directory. Returns ------- dict or None - If save_results=True: Dictionary of saved file paths - If save_results=False: None (plots are shown interactively) + If save_to_disk=True: Dictionary of saved file paths with structure: + {"html": ["path/to/html_dir/plot1.html", ...]} + If save_to_disk=False: None (plots are shown interactively) + + Notes + ----- + Output Structure: + - HTML files are saved in a directory (standardized for HTML outputs) + - When save_to_disk=False, plots are shown interactively + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["html"]) # List of HTML file paths + >>> # ['./html_dir/plot_1.html', './html_dir/plot_2.html'] + + >>> # Display plots interactively without saving + >>> run_from_json("params.json", save_to_disk=False) + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") """ # Parse parameters from JSON params = parse_params(json_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + # HTML uses directory type per standardized schema + if "outputs" not in params: + params["outputs"] = { + "html": {"type": "directory", "name": "html_dir"} + } + # Load the upstream analysis data adata = load_input(params["Upstream_Analysis"]) + # Extract parameters color_by = params["Color_By"] annotations = params.get("Annotation_s_to_Highlight", [""]) feature = params.get("Feature_to_Highlight", "None") @@ -81,6 +130,7 @@ def run_from_json( flip_y = params.get("Flip_Vertical_Axis", False) + # Process parameters feature = text_to_value(feature) if color_by == "Annotation": feature = None @@ -96,6 +146,7 @@ def run_from_json( layer = text_to_value(layer, "Original") + # Execute the interactive spatial plot result_list = interactive_spatial_plot( adata=adata, annotations=annotations, @@ -115,10 +166,10 @@ def run_from_json( cmax=cmax ) - # Handle results based on save_results flag - if save_results: - saved_files = {} - output_prefix = params.get("Output_File", "interactive_plot") + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare HTML outputs as a dictionary for directory saving + html_dict = {} for result in result_list: image_name = result['image_name'] @@ -130,16 +181,25 @@ def run_from_json( # Convert to HTML html_content = pio.to_html(image_object, full_html=True) - # Save HTML file - html_filename = f"{output_prefix}_{image_name}.html" - with open(html_filename, 'w') as file: - file.write(html_content) - - saved_files[html_filename] = html_filename + # Add to dictionary with appropriate name + html_dict[image_name] = html_content + + # Prepare results dictionary based on outputs config + results_dict = {} + if "html" in params["outputs"]: + results_dict["html"] = html_dict + + # Use centralized save_results function + # All file handling and logging is now done by save_results + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) print( f"Interactive Spatial Plot completed → " - f"{list(saved_files.keys())}" + f"{saved_files.get('html', [])}" ) return saved_files else: @@ -147,23 +207,35 @@ def run_from_json( for result in result_list: result['image_object'].show() + print("Displayed interactive plots without saving") return None # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python interactive_spatial_plot_template.py ", + "Usage: python interactive_spatial_plot_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nDisplayed interactive plots") \ No newline at end of file + print("\nDisplayed interactive plots") diff --git a/src/spac/templates/load_csv_files_template.py b/src/spac/templates/load_csv_files_template.py new file mode 100644 index 00000000..0bb7cf87 --- /dev/null +++ b/src/spac/templates/load_csv_files_template.py @@ -0,0 +1,94 @@ +""" +Platform-agnostic Load CSV Files template converted from NIDAP. +Handles both Galaxy (list of file paths) and NIDAP (directory path) inputs. + +Usage +----- +>>> from spac.templates.load_csv_files_template import run_from_json +>>> run_from_json("examples/load_csv_params.json") +""" +import sys +from pathlib import Path +from typing import Any, Dict, Union +import pandas as pd +import logging + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.templates.template_utils import ( + save_results, + parse_params, + load_csv_files, +) + +logger = logging.getLogger(__name__) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_to_disk: bool = True, + output_dir: str = None, +) -> Union[Dict[str, str], pd.DataFrame]: + """ + Execute Load CSV Files analysis with parameters from JSON. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file or parameter dictionary + save_to_disk : bool, optional + Whether to save results to disk. Default is True. + output_dir : str, optional + Base directory for outputs. + + Returns + ------- + dict or DataFrame + If save_to_disk=True: Dictionary of saved file paths + If save_to_disk=False: The processed DataFrame + """ + params = parse_params(json_path) + + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + if "outputs" not in params: + params["outputs"] = {"dataframe": {"type": "file", "name": "dataframe.csv"}} + + # Load configuration + files_config = pd.read_csv(params["CSV_Files_Configuration"]) + + # Load and combine CSV files using centralized utility + final_df = load_csv_files( + csv_input=params["CSV_Files"], + files_config=files_config, + string_columns=params.get("String_Columns", []) + ) + + logger.info(f"Load CSV Files completed: {final_df.shape}") + + # Save or return results + if save_to_disk: + saved_files = save_results( + results={"dataframe": final_df}, + params=params, + output_base_dir=output_dir + ) + return saved_files + else: + return final_df + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python load_csv_files_template.py [output_dir]") + sys.exit(1) + + logging.basicConfig(level=logging.INFO) + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + result = run_from_json(sys.argv[1], output_dir=output_dir) + + if isinstance(result, dict): + for key, path in result.items(): + print(f"{key}: {path}") diff --git a/src/spac/templates/manual_phenotyping_template.py b/src/spac/templates/manual_phenotyping_template.py index c626d7a2..85f11024 100644 --- a/src/spac/templates/manual_phenotyping_template.py +++ b/src/spac/templates/manual_phenotyping_template.py @@ -3,6 +3,9 @@ Platform-agnostic Manual Phenotyping template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.manual_phenotyping_template import run_from_json @@ -13,20 +16,22 @@ from pathlib import Path from typing import Any, Dict, Union, List, Optional import pandas as pd +import logging # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) from spac.phenotyping import assign_manual_phenotypes from spac.templates.template_utils import ( - save_outputs, - parse_params + save_results, + parse_params, ) def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], pd.DataFrame]: """ Execute Manual Phenotyping analysis with parameters from JSON. @@ -35,35 +40,120 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the DataFrame + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Dataset": "path/to/dataframe.csv", + "Phenotypes_Code": "path/to/phenotypes.csv", + "Classification_Column_Prefix": "", + "Classification_Column_Suffix": "", + "Allow_Multiple_Phenotypes": true, + "Manual_Annotation_Name": "manual_phenotype", + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If True, saves the DataFrame with + phenotype annotations to a CSV file. If False, returns the DataFrame directly for in-memory workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. All outputs will be saved relative to this directory. Returns ------- dict or DataFrame - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed DataFrame + If save_to_disk=True: Dictionary of saved file paths with structure: + { + "dataframe": "path/to/dataframe.csv" + } + If save_to_disk=False: The processed DataFrame with phenotype annotations + + Notes + ----- + Output Structure: + - DataFrame is saved as a single CSV file + - When save_to_disk=False, the DataFrame is returned for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["dataframe"]) # Path to saved CSV file + + >>> # Get results in memory + >>> phenotyped_df = run_from_json("params.json", save_to_disk=False) + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") """ # Parse parameters from JSON params = parse_params(json_path) - # Load input data - support both DataFrame and CSV file + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + if "outputs" not in params: + params["outputs"] = { + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + + # Load upstream data - DataFrame or CSV file upstream = params['Upstream_Dataset'] if isinstance(upstream, pd.DataFrame): - dataframe = upstream # Direct DataFrame pass from previous step + dataframe = upstream # Direct DataFrame from previous step + elif isinstance(upstream, (str, Path)): + # Galaxy passes .dat files, but they contain CSV data + # Don't check extension - directly read as CSV + path = Path(upstream) + try: + dataframe = pd.read_csv(path) + logging.info(f"Successfully loaded CSV data from: {path}") + except Exception as e: + raise ValueError( + f"Failed to read CSV data from '{path}'. " + f"This tool expects CSV/tabular format. " + f"Error: {str(e)}" + ) else: - dataframe = pd.read_csv(upstream) # Read from CSV file + raise TypeError( + f"Upstream_Dataset must be DataFrame or file path. " + f"Got {type(upstream)}" + ) + + # Load phenotypes code - DataFrame or CSV file + phenotypes_input = params['Phenotypes_Code'] + if isinstance(phenotypes_input, pd.DataFrame): + phenotypes = phenotypes_input + elif isinstance(phenotypes_input, (str, Path)): + # Galaxy passes .dat files, but they contain CSV data + # Don't check extension - directly read as CSV + path = Path(phenotypes_input) + try: + phenotypes = pd.read_csv(path) + logging.info(f"Successfully loaded phenotypes from: {path}") + except Exception as e: + raise ValueError( + f"Failed to read CSV data from '{path}'. " + f"This tool expects CSV/tabular format. " + f"Error: {str(e)}" + ) + else: + raise TypeError( + f"Phenotypes_Code must be DataFrame or file path. " + f"Got {type(phenotypes_input)}" + ) - # dataframe = {{{Upstream_Dataset}}} # Already loaded above - phenotypes = pd.read_csv(params['Phenotypes_Code']) + # Extract parameters prefix = params.get('Classification_Column_Prefix', '') suffix = params.get('Classification_Column_Suffix', '') multiple = params.get('Allow_Multiple_Phenotypes', True) manual_annotation = params.get('Manual_Annotation_Name', 'manual_phenotype') - print(phenotypes) + logging.info(f"Phenotypes configuration:\n{phenotypes}") # returned_dic is not used, but copy from original NIDAP logic returned_dic = assign_manual_phenotypes( @@ -76,42 +166,71 @@ def run_from_json( ) # The dataframe changes in place - # --------- Original NIDAP Logic End --------- # # Print summary statistics phenotype_counts = dataframe[manual_annotation].value_counts() - print(f"\nPhenotype distribution:") - print(phenotype_counts) + logging.info(f"\nPhenotype distribution:\n{phenotype_counts}") - print("\nManual Phenotyping completed successfully.") + logging.info("\nManual Phenotyping completed successfully.") - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "manual_phenotyped.csv") - saved_files = save_outputs({output_file: dataframe}) + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} - print(f"Manual Phenotyping completed → {saved_files[output_file]}") + # Check for dataframe output + if "dataframe" in params["outputs"]: + results_dict["dataframe"] = dataframe + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + logging.info("Manual Phenotyping analysis completed successfully.") return saved_files else: # Return the DataFrame directly for in-memory workflows - print("Returning DataFrame (not saving to file)") + logging.info("Returning DataFrame for in-memory use") return dataframe # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python manual_phenotyping_template.py ", + "Usage: python manual_phenotyping_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - saved_files = run_from_json(sys.argv[1]) + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) - if isinstance(saved_files, dict): + # Display results based on return type + if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in saved_files.items(): - print(f" {filename}: {filepath}") \ No newline at end of file + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") + else: + print("\nReturned DataFrame") + print(f"DataFrame shape: {result.shape}") diff --git a/src/spac/templates/nearest_neighbor_calculation_template.py b/src/spac/templates/nearest_neighbor_calculation_template.py index 57417b1a..45dabb71 100644 --- a/src/spac/templates/nearest_neighbor_calculation_template.py +++ b/src/spac/templates/nearest_neighbor_calculation_template.py @@ -9,28 +9,30 @@ ... ) >>> run_from_json("examples/nearest_neighbor_calculation_params.json") """ -import json +import logging import sys from pathlib import Path from typing import Any, Dict, Union -import pandas as pd # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) -from spac.utils import check_table, check_annotation from spac.spatial_analysis import calculate_nearest_neighbor from spac.templates.template_utils import ( load_input, - save_outputs, parse_params, + save_results, text_to_value, ) +# Set up logging +logger = logging.getLogger(__name__) + def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: Union[str, Path] = None ) -> Union[Dict[str, str], Any]: """ Execute Nearest Neighbor Calculation analysis with parameters from JSON. @@ -39,20 +41,66 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the adata object + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/input.pickle", + "Annotation": "cell_type", + "ImageID": "None", + "Nearest_Neighbor_Associated_Table": "spatial_distance", + "Verbose": true, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If False, returns the adata object directly for in-memory workflows. Default is True. + output_dir : str or Path, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. All outputs will be saved relative to this directory. Returns ------- dict or AnnData - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed AnnData object + If save_to_disk=True: Dictionary of saved file paths with structure: + {"analysis": "path/to/output.pickle"} + If save_to_disk=False: The processed AnnData object + + Notes + ----- + Output Structure: + - Analysis output is saved as a single pickle file (standardized for analysis outputs) + - When save_to_disk=False, the AnnData object is returned for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["analysis"]) # Path to saved pickle file + >>> # './output.pickle' + + >>> # Get results in memory for further processing + >>> adata = run_from_json("params.json", save_to_disk=False) + >>> # Can now work with adata object directly + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") """ # Parse parameters from JSON params = parse_params(json_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + # Analysis uses file type per standardized schema + if "outputs" not in params: + params["outputs"] = { + "analysis": {"type": "file", "name": "output.pickle"} + } + # Load the upstream analysis data adata = load_input(params["Upstream_Analysis"]) @@ -68,14 +116,14 @@ def run_from_json( # Convert any string "None" to actual None for Python imageid = text_to_value(imageid, default_none_text="None") - print( + logger.info( "Running `calculate_nearest_neighbor` with the following parameters:" ) - print(f" annotation: {annotation}") - print(f" spatial_associated_table: {spatial_associated_table}") - print(f" imageid: {imageid}") - print(f" label: {label}") - print(f" verbose: {verbose}") + logger.info(f" annotation: {annotation}") + logger.info(f" spatial_associated_table: {spatial_associated_table}") + logger.info(f" imageid: {imageid}") + logger.info(f" label: {label}") + logger.info(f" verbose: {verbose}") # Perform the nearest neighbor calculation calculate_nearest_neighbor( @@ -87,52 +135,73 @@ def run_from_json( verbose=verbose ) - print("Nearest neighbor calculation complete.") - print("adata.obsm keys:", list(adata.obsm.keys())) + logger.info("Nearest neighbor calculation complete.") + logger.info(f"adata.obsm keys: {list(adata.obsm.keys())}") if label in adata.obsm: - print( - f"Preview of adata.obsm['{label}']:\n", - adata.obsm[label].head() + logger.info( + f"Preview of adata.obsm['{label}']:\n{adata.obsm[label].head()}" ) - print(adata) + logger.info(f"{adata}") - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "transform_output.pickle") - # Default to pickle format if no recognized extension - if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): - output_file = output_file + '.pickle' + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} - saved_files = save_outputs({output_file: adata}) + if "analysis" in params["outputs"]: + results_dict["analysis"] = adata - print( + # Use centralized save_results function + # All file handling and logging is now done by save_results + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + logger.info( f"Nearest Neighbor Calculation completed → " - f"{saved_files[output_file]}" + f"{saved_files['analysis']}" ) return saved_files else: # Return the adata object directly for in-memory workflows - print("Returning AnnData object (not saving to file)") + logger.info("Returning AnnData object (not saving to file)") return adata # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( "Usage: python nearest_neighbor_calculation_template.py " - "", + " [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + # Display results based on return type if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, path in result.items(): + print(f" {key}: {path}") else: - print("\nReturned AnnData object") \ No newline at end of file + print("\nReturned AnnData object for in-memory use") + print(f"AnnData: {result}") + print(f"Shape: {result.shape}") diff --git a/src/spac/templates/neighborhood_profile_template.py b/src/spac/templates/neighborhood_profile_template.py index 0bc74861..fabe5e21 100644 --- a/src/spac/templates/neighborhood_profile_template.py +++ b/src/spac/templates/neighborhood_profile_template.py @@ -20,7 +20,7 @@ from spac.spatial_analysis import neighborhood_profile from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, text_to_value, ) @@ -28,7 +28,8 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: Union[str, Path] = None ) -> Union[Dict[str, str], Dict[Tuple[str, str], pd.DataFrame]]: """ Execute Neighborhood Profile analysis with parameters from JSON. @@ -38,19 +39,32 @@ def run_from_json( ---------- json_path : str, Path, or dict Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional + save_to_disk : bool, optional Whether to save results to file. If False, returns the dataframes directly for in-memory workflows. Default is True. + output_dir : str or Path, optional + Output directory for results. If None, uses params['Output_Directory'] or '.' Returns ------- dict - If save_results=True: Dictionary of saved file paths - If save_results=False: Dictionary of (anchor, neighbor) tuples + If save_to_disk=True: Dictionary of saved file paths + If save_to_disk=False: Dictionary of (anchor, neighbor) tuples to DataFrames """ # Parse parameters from JSON params = parse_params(json_path) + + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + # Neighborhood Profile dataframes use directory type per special case in template_utils + if "outputs" not in params: + params["outputs"] = { + "dataframe": {"type": "directory", "name": "dataframe_dir"} + } # Load the upstream analysis data adata = load_input(params["Upstream_Analysis"]) @@ -69,9 +83,7 @@ def run_from_json( ] # Call the spatial umap calculation - bins = [float(radius) for radius in bins] - slide_names = text_to_value(slide_names) neighborhood_profile( @@ -97,26 +109,35 @@ def run_from_json( output_table ) - # Handle results based on save_results flag - if save_results: - # Save outputs - saved_files = {} + # Handle results based on save_to_disk flag + if save_to_disk: + # Package dataframes in a dictionary for directory saving + # This ensures they're saved in a directory per standardized schema + results_dict = {} + # Create a dictionary of dataframes with their filenames as keys + dataframe_dict = {} for (anchor_label, neighbor_label), filename in zip( dataframes.keys(), filenames ): df = dataframes[(anchor_label, neighbor_label)] - saved_files[filename] = df - - # Save all CSV files - saved_files = save_outputs(saved_files) + # Remove .csv extension as save_results will add it + key = filename.replace('.csv', '') + dataframe_dict[key] = df - for filename in saved_files: - print(f"Saved: {filename}") + # Store in results with "dataframe" key to match outputs config + if "dataframe" in params["outputs"]: + results_dict["dataframe"] = dataframe_dict - print( - f"Neighborhood Profile completed → {len(saved_files)} files" + # Use centralized save_results function + # All file handling and logging is now done by save_results + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir ) + + print(f"Neighborhood Profile completed → {len(saved_files.get('dataframe', []))} files") return saved_files else: # Return the dataframes directly for in-memory workflows @@ -220,18 +241,32 @@ def neighborhood_profiles_for_pairs( # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python neighborhood_profile_template.py ", + "Usage: python neighborhood_profile_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths[:3]: # Show first 3 files + print(f" - {path}") + if len(paths) > 3: + print(f" ... and {len(paths) - 3} more files") + else: + print(f" {key}: {paths}") else: - print("\nReturned dataframes") \ No newline at end of file + print("\nReturned dataframes for in-memory use") diff --git a/src/spac/templates/normalize_batch_template.py b/src/spac/templates/normalize_batch_template.py index aa746396..73ef838e 100644 --- a/src/spac/templates/normalize_batch_template.py +++ b/src/spac/templates/normalize_batch_template.py @@ -2,6 +2,9 @@ Platform-agnostic Normalize Batch template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.normalize_batch_template import run_from_json @@ -9,6 +12,7 @@ """ import json import sys +import logging from pathlib import Path from typing import Any, Dict, Union @@ -18,14 +22,17 @@ from spac.transformations import batch_normalize from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, ) +logger = logging.getLogger(__name__) + def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], Any]: """ Execute Normalize Batch analysis with parameters from JSON. @@ -34,20 +41,53 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the adata object + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Annotation": "batch_column", + "Input_Table_Name": "Original", + "Output_Table_Name": "batch_normalized_table", + "Normalization_Method": "median", + "Take_Log": false, + "Need_Normalization": true, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If False, returns the adata object directly for in-memory workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. Returns ------- dict or AnnData - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed AnnData object + If save_to_disk=True: Dictionary of saved file paths with structure: + {"analysis": "path/to/output.pickle"} + If save_to_disk=False: The processed AnnData object + + Notes + ----- + Output Structure: + - Analysis output is saved as a single pickle file + - When save_to_disk=False, the AnnData object is returned for programmatic use """ # Parse parameters from JSON params = parse_params(json_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + if "outputs" not in params: + params["outputs"] = { + "analysis": {"type": "file", "name": "output.pickle"} + } + # Load the upstream analysis data all_data = load_input(params["Upstream_Analysis"]) @@ -61,7 +101,7 @@ def run_from_json( output_layer = params.get("Output_Table_Name", "batch_normalized_table") method = params.get("Normalization_Method", "median") take_log = params.get("Take_Log", False) - + need_normalization = params.get("Need_Normalization", False) if need_normalization: batch_normalize( @@ -73,45 +113,75 @@ def run_from_json( log=take_log ) - print("Statistics of original data:\n", all_data.to_df().describe()) - print("Statistics of layer data:\n", all_data.to_df(layer=output_layer).describe()) + logger.info( + f"Statistics of original data:\n{all_data.to_df().describe()}" + ) + logger.info( + f"Statistics of layer data:\n" + f"{all_data.to_df(layer=output_layer).describe()}" + ) else: - print("Statistics of original data:\n", all_data.to_df().describe()) - - print("Current Analysis contains:\n", all_data) - - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "transform_output.pickle") - # Default to pickle format if no recognized extension - if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): - output_file = output_file + '.pickle' - - saved_files = save_outputs({output_file: all_data}) - - print(f"Normalize Batch completed → {saved_files[output_file]}") + logger.info( + f"Statistics of original data:\n{all_data.to_df().describe()}" + ) + + logger.info(f"Current Analysis contains:\n{all_data}") + + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + if "analysis" in params["outputs"]: + results_dict["analysis"] = all_data + + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + logger.info("Normalize Batch analysis completed successfully.") return saved_files else: # Return the adata object directly for in-memory workflows - print("Returning AnnData object (not saving to file)") + logger.info("Returning AnnData object for in-memory use") return all_data # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python normalize_batch_template.py ", + "Usage: python normalize_batch_template.py " + "[output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nReturned AnnData object") \ No newline at end of file + print("\nReturned AnnData object") diff --git a/src/spac/templates/phenograph_clustering_template.py b/src/spac/templates/phenograph_clustering_template.py index 820a768a..99d84f62 100644 --- a/src/spac/templates/phenograph_clustering_template.py +++ b/src/spac/templates/phenograph_clustering_template.py @@ -2,6 +2,9 @@ Platform-agnostic Phenograph Clustering template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Follows standardized output schema where analysis is saved as a file. + Usage ----- >>> from spac.templates.phenograph_clustering_template import run_from_json @@ -19,7 +22,7 @@ from spac.transformations import phenograph_clustering from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, text_to_value, ) @@ -27,7 +30,8 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], Any]: """ Execute Phenograph Clustering analysis with parameters from JSON. @@ -36,20 +40,64 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the adata object + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Table_to_Process": "Original", + "K_Nearest_Neighbors": 30, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If False, returns the AnnData object directly for in-memory workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. Returns ------- dict or AnnData - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed AnnData object + If save_to_disk=True: Dictionary of saved file paths with structure: + {"analysis": "path/to/output.pickle"} + If save_to_disk=False: The processed AnnData object + + Notes + ----- + Output Structure: + - Analysis output is saved as a single pickle file (standardized for analysis outputs) + - When save_to_disk=False, the AnnData object is returned for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["analysis"]) # Path to saved pickle file + >>> # './output.pickle' + + >>> # Get results in memory + >>> adata = run_from_json("params.json", save_to_disk=False) + >>> # Can now work with adata object directly + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") """ # Parse parameters from JSON params = parse_params(json_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + # Analysis uses file type per standardized schema + if "outputs" not in params: + params["outputs"] = { + "analysis": {"type": "file", "name": "output.pickle"} + } + # Load the upstream analysis data adata = load_input(params["Upstream_Analysis"]) @@ -98,19 +146,24 @@ def run_from_json( print(label_counts) print("\n") - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "transform_output.pickle") - # Default to pickle format if no recognized extension - if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): - output_file = output_file + '.pickle' + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary + results_dict = {} + if "analysis" in params["outputs"]: + results_dict["analysis"] = adata - saved_files = save_outputs({output_file: adata}) + # Use centralized save_results function + # All file handling and logging is now done by save_results + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) print( f"Phenograph Clustering completed → " - f"{saved_files[output_file]}" + f"{saved_files['analysis']}" ) return saved_files else: @@ -121,18 +174,24 @@ def run_from_json( # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python phenograph_clustering_template.py ", + "Usage: python phenograph_clustering_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) if isinstance(result, dict): print("\nOutput files:") for filename, filepath in result.items(): print(f" {filename}: {filepath}") else: - print("\nReturned AnnData object") \ No newline at end of file + print("\nReturned AnnData object") diff --git a/src/spac/templates/posit_it_python_template.py b/src/spac/templates/posit_it_python_template.py index 716062d3..2b4bf440 100644 --- a/src/spac/templates/posit_it_python_template.py +++ b/src/spac/templates/posit_it_python_template.py @@ -2,32 +2,71 @@ Platform-agnostic Post-It-Python template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.posit_it_python_template import run_from_json >>> run_from_json("examples/posit_it_python_params.json") """ -import json import sys from pathlib import Path -from typing import Any, Dict, Union, Optional, Tuple +from typing import Any, Dict, Union, List +import logging import matplotlib.pyplot as plt # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) from spac.templates.template_utils import ( - save_outputs, + save_results, parse_params, text_to_value, ) +# Color palette mapping color names to hex codes +PAINTS = { + 'White': '#FFFFFF', + 'LightGrey': '#D3D3D3', + 'Grey': '#999999', + 'Black': '#000000', + 'Red1': '#F44E3B', + 'Red2': '#D33115', + 'Red3': '#9F0500', + 'Orange1': '#FE9200', + 'Orange2': '#E27300', + 'Orange3': '#C45100', + 'Yellow1': '#FCDC00', + 'Yellow2': '#FCC400', + 'Yellow3': '#FB9E00', + 'YellowGreen1': '#DBDF00', + 'YellowGreen2': '#B0BC00', + 'YellowGreen3': '#808900', + 'Green1': '#A4DD00', + 'Green2': '#68BC00', + 'Green3': '#194D33', + 'Teal1': '#68CCCA', + 'Teal2': '#16A5A5', + 'Teal3': '#0C797D', + 'Blue1': '#73D8FF', + 'Blue2': '#009CE0', + 'Blue3': '#0062B1', + 'Purple1': '#AEA1FF', + 'Purple2': '#7B64FF', + 'Purple3': '#653294', + 'Magenta1': '#FDA1FF', + 'Magenta2': '#FA28FF', + 'Magenta3': '#AB149E', +} + def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True, - show_plot: bool = False -) -> Union[Dict[str, str], Any]: + save_to_disk: bool = True, + show_plot: bool = False, + output_dir: str = None, +) -> Union[Dict[str, Union[str, List[str]]], plt.Figure]: """ Execute Post-It-Python analysis with parameters from JSON. Replicates the NIDAP template functionality exactly. @@ -35,22 +74,49 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the figure + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Label": "Post-It", + "Label_font_color": "Black", + "Label_font_size": "80", + ... + "outputs": { + "figures": {"type": "directory", "name": "figures_dir"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If False, returns the figure directly for in-memory workflows. Default is True. show_plot : bool, optional Whether to display the plot. Default is False. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. Returns ------- - dict or figure - If save_results=True: Dictionary of saved file paths - If save_results=False: The matplotlib figure object + dict or Figure + If save_to_disk=True: Dictionary of saved file paths + If save_to_disk=False: The matplotlib figure object """ + # Set up logging + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + # Parse parameters from JSON params = parse_params(json_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + if "outputs" not in params: + params["outputs"] = { + "figures": {"type": "directory", "name": "figures_dir"} + } + # Extract parameters using .get() with defaults from JSON template text = params.get("Label", "Post-It") text_color = params.get("Label_font_color", "Black") @@ -62,143 +128,140 @@ def run_from_json( # background params fill_color = params.get("Background_fill_color", "Yellow1") fill_alpha = params.get("Background_fill_opacity", "10") - border_alpha = 1 - border_width = 0 # image params image_width = params.get("Page_width", "18") image_height = params.get("Page_height", "6") image_resolution = params.get("Page_DPI", "300") - # output value - tag = "CCBR" - # Convert string parameters to appropriate types text_size = text_to_value( text_size, to_int=True, param_name="Label_font_size" ) - + bold = text_to_value(bold) == "True" - + fill_alpha = text_to_value( fill_alpha, to_float=True, param_name="Background_fill_opacity" ) - + image_width = text_to_value( image_width, to_float=True, param_name="Page_width" ) - + image_height = text_to_value( image_height, to_float=True, param_name="Page_height" ) - + image_resolution = text_to_value( image_resolution, to_int=True, param_name="Page_DPI" ) - # RUN ==== - - # paints - paints = { - 'White': '#FFFFFF', - 'LightGrey': '#D3D3D3', - 'Grey': '#999999', - 'Black': '#000000', - 'Red1': '#F44E3B', - 'Red2': '#D33115', - 'Red3': '#9F0500', - 'Orange1': '#FE9200', - 'Orange2': '#E27300', - 'Orange3': '#C45100', - 'Yellow1': '#FCDC00', - 'Yellow2': '#FCC400', - 'Yellow3': '#FB9E00', - 'YellowGreen1': '#DBDF00', - 'YellowGreen2': '#B0BC00', - 'YellowGreen3': '#808900', - 'Green1': '#A4DD00', - 'Green2': '#68BC00', - 'Green3': '#194D33', - 'Teal1': '#68CCCA', - 'Teal2': '#16A5A5', - 'Teal3': '#0C797D', - 'Blue1': '#73D8FF', - 'Blue2': '#009CE0', - 'Blue3': '#0062B1', - 'Purple1': '#AEA1FF', - 'Purple2': '#7B64FF', - 'Purple3': '#653294', - 'Magenta1': '#FDA1FF', - 'Magenta2': '#FA28FF', - 'Magenta3': '#AB149E' - } - - # image: png + # RUN ==== + + # Create figure fig = plt.figure( - figsize=(image_width, image_height), + figsize=(image_width, image_height), dpi=image_resolution ) - fig.patch.set_facecolor(paints[fill_color]) - fig.patch.set_alpha(fill_alpha/100) + fig.patch.set_facecolor(PAINTS[fill_color]) + fig.patch.set_alpha(fill_alpha / 100) for ax in fig.get_axes(): for item in ([ax.title, ax.xaxis.label, ax.yaxis.label] + - ax.get_xticklabels() + ax.get_yticklabels()): + ax.get_xticklabels() + ax.get_yticklabels()): item.set_fontsize(text_size) item.set_fontfamily(text_fontfamily) item.set_fontstyle(text_fontface) if bold: - item.set_fontweight('bold') + item.set_fontweight('bold') fig.text( - 0.5, 0.5, text, - fontsize=text_size, - color=paints[text_color], - ha='center', - va='center', + 0.5, 0.5, text, + fontsize=text_size, + color=PAINTS[text_color], + ha='center', + va='center', fontfamily=text_fontfamily, - fontstyle=text_fontface, + fontstyle=text_fontface, fontweight='bold' if bold else 'normal' ) - + if show_plot: plt.show() - - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "graphicsFile.png") - # Ensure .png extension - if not output_file.endswith('.png'): - output_file = output_file + '.png' - - fig.savefig( - output_file, - format='png', - transparent=True, - bbox_inches='tight' + + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + if "figures" in params["outputs"]: + results_dict["figures"] = {"postit": fig} + + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir ) + + # Close figure after saving plt.close(fig) - - saved_files = {output_file: output_file} - - print(f"Post-It-Python completed → {saved_files[output_file]}") + + logger.info("Post-It-Python completed successfully.") return saved_files else: # Return the figure object directly for in-memory workflows - print("Returning figure object (not saving to file)") + logger.info("Returning figure object for in-memory use") return fig +# CLI interface +if __name__ == "__main__": + if len(sys.argv) < 2: + print( + "Usage: python posit_it_python_template.py " + " [output_dir]", + file=sys.stderr + ) + sys.exit(1) + + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + + if isinstance(result, dict): + print("\nOutput files:") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") + else: + print(f"\nReturned figure object") + + # CLI interface if __name__ == "__main__": if len(sys.argv) != 2: diff --git a/src/spac/templates/quantile_scaling_template.py b/src/spac/templates/quantile_scaling_template.py index 205df31c..48cc8bf8 100644 --- a/src/spac/templates/quantile_scaling_template.py +++ b/src/spac/templates/quantile_scaling_template.py @@ -2,6 +2,9 @@ Platform-agnostic Quantile Scaling template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Follows standardized output schema where html outputs are saved as directories. + Usage ----- >>> from spac.templates.quantile_scaling_template import run_from_json @@ -10,7 +13,8 @@ import json import sys from pathlib import Path -from typing import Any, Dict, Union, Tuple +from typing import Any, Dict, Union, List, Tuple +import logging import pandas as pd import plotly.graph_objects as go @@ -20,7 +24,7 @@ from spac.transformations import normalize_features from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, text_to_value, ) @@ -28,9 +32,10 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True, - show_plot: bool = True -) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: + save_to_disk: bool = True, + show_plot: bool = True, + output_dir: str = None, +) -> Union[Dict[str, Union[str, List[str]]], Tuple[Any, go.Figure]]: """ Execute Quantile Scaling analysis with parameters from JSON. Replicates the NIDAP template functionality exactly. @@ -38,23 +43,45 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the adata object + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Low_Quantile": "0.02", + "High_Quantile": "0.98", + "Interpolation": "nearest", + "Table_to_Process": "Original", + "Output_Table_Name": "normalized_feature", + "Per_Batch": "False", + "Annotation": null, + "outputs": { + "analysis": {"type": "file", "name": "quantile_scaled_data.pickle"}, + "html": {"type": "directory", "name": "normalization_summary"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If False, returns the adata object and figure directly for in-memory workflows. Default is True. show_plot : bool, optional Whether to display the plot. Default is True. + output_dir : str, optional + Override output directory from params. Default uses params value. Returns ------- dict or tuple - If save_results=True: Dictionary of saved file paths - If save_results=False: Tuple of (adata, figure) + If save_to_disk=True: Dictionary of saved file paths + If save_to_disk=False: Tuple of (adata, figure) """ + # Set up logging + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + # Parse parameters from JSON params = parse_params(json_path) # Load the upstream analysis data + logger.info(f"Loading upstream analysis data from {params['Upstream_Analysis']}") adata = load_input(params["Upstream_Analysis"]) # Extract parameters using .get() with defaults from JSON template @@ -97,16 +124,15 @@ def run_from_json( ) # Check if output_layer already exists in adata - print(f"Checking if output layer '{output_layer}' exists in adata " - f"layers...") + logger.info(f"Checking if output layer '{output_layer}' exists in adata layers...") if output_layer in adata.layers.keys(): raise ValueError( f"Output Table Name '{output_layer}' already exists, " f"please rename it." ) else: - print(f"Output layer '{output_layer}' does not exist. " - f"Proceeding with normalization.") + logger.info(f"Output layer '{output_layer}' does not exist. " + f"Proceeding with normalization.") def df_as_html( df, @@ -184,8 +210,8 @@ def create_normalization_info( return normalization_info - print(f"High quantile used: {str(high_quantile)}") - print(f"Low quantile used: {str(low_quantile)}") + logger.info(f"High quantile used: {str(high_quantile)}") + logger.info(f"Low quantile used: {str(low_quantile)}") transformed_data = normalize_features( adata=adata, @@ -198,9 +224,9 @@ def create_normalization_info( annotation=annotation ) - print(f"Transformed data stored in layer: {output_layer}") + logger.info(f"Transformed data stored in layer: {output_layer}") dataframe = pd.DataFrame(transformed_data.layers[output_layer]) - print(dataframe.describe()) + logger.info(f"Transform summary:\n{dataframe.describe()}") normalization_info = create_normalization_info( adata, @@ -224,39 +250,70 @@ def create_normalization_info( if show_plot: html_plot.show() - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "transform_output.pickle") - # Default to pickle format if no recognized extension - if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): - output_file = output_file + '.pickle' - - saved_files = save_outputs({output_file: transformed_data}) - - print(f"Quantile Scaling completed → {saved_files[output_file]}") + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + # Add analysis output (single file) + if "analysis" in params["outputs"]: + results_dict["analysis"] = transformed_data + + # Add HTML output (directory) + if "html" in params["outputs"]: + results_dict["html"] = {"normalization_summary": html_plot} + + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + logger.info("Quantile Scaling analysis completed successfully.") return saved_files else: - # Return the adata object and figure directly for in-memory - # workflows - print("Returning AnnData object and figure (not saving to file)") + # Return the adata object and figure directly for in-memory workflows + logger.info("Returning AnnData object and figure for in-memory use") return transformed_data, html_plot # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python quantile_scaling_template.py ", + "Usage: python quantile_scaling_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + # Display results based on return type if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nReturned AnnData object and figure") \ No newline at end of file + adata, html_plot = result + print("\nReturned AnnData object and figure for in-memory use") + print(f"AnnData shape: {adata.shape}") + print(f"Output layer: {list(adata.layers.keys())}") diff --git a/src/spac/templates/relational_heatmap_template.py b/src/spac/templates/relational_heatmap_template.py index 3cd403ea..2087f5ff 100644 --- a/src/spac/templates/relational_heatmap_template.py +++ b/src/spac/templates/relational_heatmap_template.py @@ -1,83 +1,183 @@ """ -Platform-agnostic Relational Heatmap template converted from NIDAP. -Maintains the exact logic from the NIDAP template. - -Usage ------ ->>> from spac.templates.relational_heatmap_template import run_from_json ->>> run_from_json("examples/relational_heatmap_params.json") +Relational Heatmap with Plotly-matplotlib color synchronization. +Extracts actual colors from Plotly and uses them in matplotlib. """ import json import sys -import os from pathlib import Path -from typing import Any, Dict, Union, Optional, Tuple +from typing import Any, Dict, Union, Tuple import pandas as pd -import plotly.io as pio +import numpy as np +import matplotlib +matplotlib.use('Agg') import matplotlib.pyplot as plt +import matplotlib.colors as mcolors +import plotly.io as pio +import plotly.express as px -# Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) from spac.visualization import relational_heatmap from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, text_to_value, ) +def get_plotly_colorscale_as_matplotlib(plotly_colormap: str) -> mcolors.LinearSegmentedColormap: + """ + Extract actual colors from Plotly colorscale and create matplotlib colormap. + This ensures exact color matching between Plotly and matplotlib. + """ + # Get Plotly's colorscale + try: + # Use plotly express to get the actual color sequence + colorscale = getattr(px.colors.sequential, plotly_colormap, None) + if colorscale is None: + colorscale = getattr(px.colors.diverging, plotly_colormap, None) + if colorscale is None: + colorscale = getattr(px.colors.cyclical, plotly_colormap, None) + + if colorscale is None: + # Fallback to a default + print(f"Warning: Could not find Plotly colorscale '{plotly_colormap}', using default") + colorscale = px.colors.sequential.Viridis + + # Convert to matplotlib colormap + if isinstance(colorscale, list): + # Create custom colormap from color list + cmap = mcolors.LinearSegmentedColormap.from_list( + f"plotly_{plotly_colormap}", + colorscale + ) + return cmap + except Exception as e: + print(f"Error extracting Plotly colors: {e}") + + # Fallback to matplotlib's viridis + return plt.cm.viridis + + +def create_matplotlib_heatmap_matching_plotly( + data: pd.DataFrame, + plotly_fig: Any, + source_annotation: str, + target_annotation: str, + colormap_name: str, + figsize: tuple, + dpi: int, + font_size: int +) -> plt.Figure: + """ + Create matplotlib heatmap that matches Plotly's appearance. + Extracts color information from the Plotly figure. + """ + fig, ax = plt.subplots(figsize=figsize, dpi=dpi) + + # Get the actual colormap from Plotly + cmap = get_plotly_colorscale_as_matplotlib(colormap_name) + + # Extract data range from Plotly figure if possible + try: + zmin = plotly_fig.data[0].zmin if hasattr(plotly_fig.data[0], 'zmin') else data.min().min() + zmax = plotly_fig.data[0].zmax if hasattr(plotly_fig.data[0], 'zmax') else data.max().max() + except: + zmin, zmax = data.min().min(), data.max().max() + + # Create heatmap matching Plotly's style + im = ax.imshow( + data.values, + aspect='auto', + cmap=cmap, + interpolation='nearest', + vmin=zmin, + vmax=zmax + ) + + # Match Plotly's tick placement + ax.set_xticks(np.arange(len(data.columns))) + ax.set_yticks(np.arange(len(data.index))) + ax.set_xticklabels(data.columns, rotation=45, ha='right', fontsize=font_size) + ax.set_yticklabels(data.index, fontsize=font_size) + + # Add colorbar + cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04) + cbar.set_label('Count', fontsize=font_size) + cbar.ax.tick_params(labelsize=font_size) + + # Title matching Plotly + ax.set_title( + f'Relational Heatmap: {source_annotation} vs {target_annotation}', + fontsize=font_size + 2, + pad=20 + ) + ax.set_xlabel(target_annotation, fontsize=font_size) + ax.set_ylabel(source_annotation, fontsize=font_size) + + # Add grid for clarity (like Plotly) + ax.set_xticks(np.arange(len(data.columns) + 1) - 0.5, minor=True) + ax.set_yticks(np.arange(len(data.index) + 1) - 0.5, minor=True) + ax.grid(which='minor', color='gray', linestyle='-', linewidth=0.3, alpha=0.3) + ax.tick_params(which='both', length=0) + + plt.tight_layout() + return fig + + def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True -) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: - """ - Execute Relational Heatmap analysis with parameters from JSON. - Replicates the NIDAP template functionality exactly. + save_to_disk: bool = True, + output_dir: str = None, + show_static_image: bool = False +) -> Union[Dict, Tuple]: + """Execute Relational Heatmap with color-matched outputs. Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the figure and - dataframe directly for in-memory workflows. Default is True. - - Returns - ------- - dict or tuple - If save_results=True: Dictionary of saved file paths - If save_results=False: Tuple of (figure, dataframe) + Path to parameters JSON file or dict of parameters. + save_to_disk : bool, default True + Whether to save results to disk. + output_dir : str, optional + Output directory. If None, read from params. + show_static_image : bool, default False + When True, generate a static PNG figure using matplotlib. + When False (default), only produce interactive HTML output. + Disabled by default because Plotly HTML-to-PNG conversion + hangs inside the Galaxy container environment. """ - # Parse parameters from JSON + params = parse_params(json_path) - - # Load the upstream analysis data + + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + if "outputs" not in params: + params["outputs"] = { + "figures": {"type": "directory", "name": "figures_dir"}, + "html": {"type": "directory", "name": "html_dir"}, + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + + # Load data adata = load_input(params["Upstream_Analysis"]) + print(f"Data loaded: {adata.shape[0]} cells, {adata.shape[1]} genes") - # Extract parameters - annotation_columns = [ - params.get("Source_Annotation_Name", "None"), - params.get("Target_Annotation_Name", "None") - ] - dpi = params.get("Figure_DPI", 300) - width_in = params.get("Figure_Width_inch", 8) - height_in = params.get("Figure_Height_inch", 10) - width_px = width_in * 96 - print(width_px) - height_px = height_in * 96 - print(height_px) - - scale = dpi / 96 - - font_size = params.get("Font_Size", 8) + # Parameters + source_annotation = text_to_value(params.get("Source_Annotation_Name", "None")) + target_annotation = text_to_value(params.get("Target_Annotation_Name", "None")) + + dpi = float(params.get("Figure_DPI", 300)) + width_in = float(params.get("Figure_Width_inch", 8)) + height_in = float(params.get("Figure_Height_inch", 10)) + font_size = float(params.get("Font_Size", 8)) colormap = params.get("Colormap", "darkmint") - source_annotation = text_to_value(annotation_columns[0]) - - target_annotation = text_to_value(annotation_columns[1]) + print(f"Creating heatmap: {source_annotation} vs {target_annotation}") + # Run SPAC relational heatmap result_dict = relational_heatmap( adata=adata, source_annotation=source_annotation, @@ -86,84 +186,61 @@ def run_from_json( font_size=font_size ) - # extract results from function return - rhmap_file_name = result_dict['file_name'] rhmap_data = result_dict['data'] - fig = result_dict['figure'] - - # Generate temporary image name for Plotly export - import tempfile - with tempfile.NamedTemporaryFile( - delete=False, suffix='.png' - ) as tmp_file: - tmp_image_name = tmp_file.name - - pio.write_image( - fig, - tmp_image_name, - width=width_px, # Specify the width in pixels - height=height_px, - engine='kaleido', # Use the 'kaleido' engine for high DPI images - scale=scale - ) - - img = plt.imread(tmp_image_name) - static, axs = plt.subplots( - 1, 1, figsize=(width_in, height_in), dpi=dpi - ) - - # Load and display the image using Matplotlib - axs.imshow(img) - axs.axis('off') + plotly_fig = result_dict['figure'] - # Display the matplotlib static figure - plt.show() - - # Clean up temp file - if os.path.exists(tmp_image_name): - os.remove(tmp_image_name) - - # Display the Plotly interactive figure - fig.show() - - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", rhmap_file_name) - if not output_file.endswith('.csv'): - output_file = output_file + '.csv' - - saved_files = save_outputs({output_file: rhmap_data}) - - # Also save the static plot - plot_file = output_file.replace('.csv', '.png') - static.savefig(plot_file, dpi=dpi, bbox_inches='tight') - saved_files[plot_file] = plot_file - - print( - f"Relational Heatmap completed → {list(saved_files.keys())}" + # Update Plotly figure + if plotly_fig: + plotly_fig.update_layout( + width=width_in * 96, + height=height_in * 96, + font=dict(size=font_size) ) + + if save_to_disk: + results_dict = { + "html": {"relational_heatmap": pio.to_html(plotly_fig, full_html=True, include_plotlyjs='cdn')}, + "dataframe": rhmap_data + } + + if show_static_image: + # Generate static matplotlib figure matching Plotly colors. + # Disabled by default on Galaxy because Plotly HTML-to-PNG + # conversion hangs in the Galaxy container environment. + print("Creating color-matched matplotlib figure...") + static_fig = create_matplotlib_heatmap_matching_plotly( + rhmap_data, + plotly_fig, + source_annotation, + target_annotation, + colormap, + (width_in, height_in), + int(dpi), + int(font_size) + ) + results_dict["figures"] = {"relational_heatmap": static_fig} + + saved_files = save_results(results_dict, params, output_base_dir=output_dir) + + if show_static_image: + plt.close(static_fig) + + print("✓ Relational Heatmap completed") return saved_files else: - # Return the figure and dataframe directly for in-memory workflows - print("Returning figure and dataframe (not saving to file)") - return static, rhmap_data + return plotly_fig, rhmap_data -# CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: - print( - "Usage: python relational_heatmap_template.py ", - file=sys.stderr - ) + if len(sys.argv) < 2: + print("Usage: python relational_heatmap_template.py ", file=sys.stderr) + sys.exit(1) + + try: + run_from_json(sys.argv[1], save_to_disk=True) + sys.exit(0) + except Exception as e: + print(f"ERROR: {e}", file=sys.stderr) + import traceback + traceback.print_exc() sys.exit(1) - - result = run_from_json(sys.argv[1]) - - if isinstance(result, dict): - print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") - else: - print("\nReturned figure and dataframe") \ No newline at end of file diff --git a/src/spac/templates/rename_labels_template.py b/src/spac/templates/rename_labels_template.py index 6be49f78..5527e3b7 100644 --- a/src/spac/templates/rename_labels_template.py +++ b/src/spac/templates/rename_labels_template.py @@ -2,6 +2,9 @@ Platform-agnostic Rename Labels template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Follows standardized output schema. + Usage ----- >>> from spac.templates.rename_labels_template import run_from_json @@ -11,6 +14,7 @@ import sys from pathlib import Path from typing import Any, Dict, Union +import logging import pandas as pd import pickle @@ -20,7 +24,7 @@ from spac.transformations import rename_annotations from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, text_to_value, ) @@ -28,7 +32,8 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], Any]: """ Execute Rename Labels analysis with parameters from JSON. @@ -37,21 +42,38 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the adata object + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Cluster_Mapping_Dictionary": "path/to/mapping.csv", + "Source_Annotation": "original_column", + "New_Annotation": "new_column", + "outputs": { + "analysis": {"type": "file", "name": "renamed_data.pickle"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If False, returns the adata object directly for in-memory workflows. Default is True. + output_dir : str, optional + Override output directory from params. Default uses params value. Returns ------- dict or AnnData - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed AnnData object + If save_to_disk=True: Dictionary of saved file paths + If save_to_disk=False: The processed AnnData object """ + # Set up logging + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + # Parse parameters from JSON params = parse_params(json_path) # Load the upstream analysis data + logger.info(f"Loading upstream analysis data from {params['Upstream_Analysis']}") all_data = load_input(params["Upstream_Analysis"]) # Extract parameters @@ -60,18 +82,17 @@ def run_from_json( renamed_column = params.get("New_Annotation", "None") # Load the mapping dictionary CSV + logger.info(f"Loading cluster mapping dictionary from {rename_list_path}") rename_list = pd.read_csv(rename_list_path) original_column = text_to_value(original_column) renamed_column = text_to_value(renamed_column) # Create a new dictionary with the desired format - dict_list = rename_list.to_dict('records') - mappings = {d['Original']: d['New'] for d in dict_list} - print("Cluster Name Mapping: \n", mappings) + logger.info(f"Cluster Name Mapping: \n{mappings}") rename_annotations( all_data, @@ -79,48 +100,74 @@ def run_from_json( dest_annotation=renamed_column, mappings=mappings) - print("After Renaming Clusters: \n", all_data) + logger.info(f"After Renaming Clusters: \n{all_data}") # Count and display occurrences of each label in the annotation - print(f'Count of cells in the output annotation:"{renamed_column}":') + logger.info(f'Count of cells in the output annotation:"{renamed_column}":') label_counts = all_data.obs[renamed_column].value_counts() - print(label_counts) - print("\n") + logger.info(f"{label_counts}") object_to_output = all_data - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "transform_output.pickle") - # Default to pickle format if no recognized extension - if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): - output_file = output_file + '.pickle' + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} - saved_files = save_outputs({output_file: object_to_output}) + # Add analysis output (single file) + if "analysis" in params["outputs"]: + results_dict["analysis"] = object_to_output - print(f"Rename Labels completed → {saved_files[output_file]}") + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + logger.info("Rename Labels analysis completed successfully.") return saved_files else: # Return the adata object directly for in-memory workflows - print("Returning AnnData object (not saving to file)") + logger.info("Returning AnnData object for in-memory use") return object_to_output # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python rename_labels_template.py ", + "Usage: python rename_labels_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + # Display results based on return type if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nReturned AnnData object") \ No newline at end of file + print("\nReturned AnnData object") + print(f"AnnData shape: {result.shape}") + print(f"Observations columns: {list(result.obs.columns)}") diff --git a/src/spac/templates/ripley_l_calculation_template.py b/src/spac/templates/ripley_l_calculation_template.py new file mode 100644 index 00000000..68b12812 --- /dev/null +++ b/src/spac/templates/ripley_l_calculation_template.py @@ -0,0 +1,151 @@ +""" +Platform-agnostic Ripley-L template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.ripley_l_template import run_from_json +>>> run_from_json("examples/ripley_l_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, List, Optional +import logging + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.spatial_analysis import ripley_l +from spac.templates.template_utils import ( + load_input, + save_results, + parse_params, + text_to_value, + convert_to_floats +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_to_disk: bool = True, + output_dir: Optional[Union[str, Path]] = None +) -> Union[Dict[str, str], Any]: + """ + Execute Ripley-L analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_to_disk : bool, optional + Whether to save results to file. If False, returns the adata object + directly for in-memory workflows. Default is True. + output_dir : str or Path, optional + Directory for outputs. If None, uses current directory. + + Returns + ------- + dict or AnnData + If save_to_disk=True: Dictionary of saved file paths + If save_to_disk=False: The processed AnnData object + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + radii = params["Radii"] + annotation = params["Annotation"] + phenotypes = [params["Center_Phenotype"], params["Neighbor_Phenotype"]] + regions = params.get("Stratify_By", "None") + n_simulations = params.get("Number_of_Simulations", 100) + area = params.get("Area", "None") + seed = params.get("Seed", 42) + spatial_key = params.get("Spatial_Key", "spatial") + edge_correction = params.get("Edge_Correction", True) + + # Process parameters + regions = text_to_value( + regions, + default_none_text="None" + ) + + area = text_to_value( + area, + default_none_text="None", + value_to_convert_to=None, + to_float=True, + param_name='Area' + ) + + # Convert radii to floats + radii = convert_to_floats(radii) + + # Run the analysis + ripley_l( + adata, + annotation=annotation, + phenotypes=phenotypes, + distances=radii, + regions=regions, + n_simulations=n_simulations, + area=area, + seed=seed, + spatial_key=spatial_key, + edge_correction=edge_correction + ) + + logging.info("Ripley-L analysis completed successfully.") + logging.debug(f"AnnData object: {adata}") + + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + if "analysis" in params["outputs"]: + results_dict["analysis"] = adata + + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + logging.info(f"Ripley-L completed → {saved_files['analysis']}") + return saved_files + else: + # Return the adata object directly for in-memory workflows + logging.info("Returning AnnData object (not saving to file)") + return adata + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python ripley_l_template.py ", file=sys.stderr) + sys.exit(1) + + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json(sys.argv[1], output_dir=output_dir) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned AnnData object") diff --git a/src/spac/templates/sankey_plot_template.py b/src/spac/templates/sankey_plot_template.py index 60d4aa34..c34a2c81 100644 --- a/src/spac/templates/sankey_plot_template.py +++ b/src/spac/templates/sankey_plot_template.py @@ -1,17 +1,16 @@ """ -Platform-agnostic Sankey Plot template converted from NIDAP. -Maintains the exact logic from the NIDAP template. - -Usage ------ ->>> from spac.templates.sankey_plot_template import run_from_json ->>> run_from_json("examples/sankey_plot_params.json") +Production version of Sankey Plot template for Galaxy. +save files only, no show() calls, no blocking operations. """ import json import sys +import os from pathlib import Path -from typing import Any, Dict, Union, Optional, Tuple +from typing import Any, Dict, List, Union, Optional, Tuple import pandas as pd +import matplotlib +# Set non-interactive backend for Galaxy +matplotlib.use('Agg') import matplotlib.pyplot as plt import plotly.io as pio @@ -21,7 +20,7 @@ from spac.visualization import sankey_plot from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, text_to_value, ) @@ -29,141 +28,209 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True, - show_plot: bool = True -) -> Union[Dict[str, str], None]: + save_to_disk: bool = True, # Always True for Galaxy + output_dir: str = None, + show_static_image: bool = False, +) -> Union[Dict[str, Union[str, List[str]]], None]: """ - Execute Sankey Plot analysis with parameters from JSON. - Replicates the NIDAP template functionality exactly. + Execute Sankey Plot analysis for Galaxy. Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns None - since this template creates multiple plot files. Default is True. - show_plot : bool, optional - Whether to display the plot. Default is True. - - Returns - ------- - dict or None - If save_results=True: Dictionary of saved file paths - If save_results=False: None (plots are displayed but not saved) + Path to parameters JSON file or dict of parameters. + save_to_disk : bool, default True + Whether to save results to disk. Always True for Galaxy. + output_dir : str, optional + Output directory. If None, read from params. + show_static_image : bool, default False + When True, generate a static PNG placeholder figure. + When False (default), only produce interactive HTML output. + Disabled by default because Plotly HTML-to-PNG conversion + hangs inside the Galaxy container environment. """ # Parse parameters from JSON params = parse_params(json_path) + print(f"Loaded parameters for {params.get('Source_Annotation_Name')} -> {params.get('Target_Annotation_Name')}") + + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + if "outputs" not in params: + params["outputs"] = { + "figures": {"type": "directory", "name": "figures_dir"}, + "html": {"type": "directory", "name": "html_dir"} + } # Load the upstream analysis data + print("Loading upstream analysis data...") adata = load_input(params["Upstream_Analysis"]) + print(f"Data loaded: {adata.shape[0]} cells, {adata.shape[1]} genes") # Extract parameters annotation_columns = [ params.get("Source_Annotation_Name", "None"), params.get("Target_Annotation_Name", "None") ] - dpi = params.get("Figure_DPI", 300) - width_num = params.get("Figure_Width_inch", 6) - scale = dpi / 96 - width_in_pixels = width_num / scale * dpi - - height_num = params.get("Figure_Height_inch", 6) - height_in_pixels = height_num / scale * dpi - - # sort_asscend = True # unused variable + + # Parse numeric parameters with error handling + try: + dpi = float(params.get("Figure_DPI", 300)) + except (ValueError, TypeError): + dpi = 300 + print(f"Warning: Invalid DPI value, using default {dpi}") + + width_num = float(params.get("Figure_Width_inch", 6)) + height_num = float(params.get("Figure_Height_inch", 6)) + source_color_map = params.get("Source_Annotation_Color_Map", "tab20") target_color_map = params.get("Target_Annotation_Color_Map", "tab20b") - - sankey_font = params.get("Font_Size", 12) + + try: + sankey_font = float(params.get("Font_Size", 12)) + except (ValueError, TypeError): + sankey_font = 12 + print(f"Warning: Invalid font size, using default {sankey_font}") target_annotation = text_to_value(annotation_columns[1]) source_annotation = text_to_value(annotation_columns[0]) + + print(f"Creating Sankey plot: {source_annotation} -> {target_annotation}") + # Execute the sankey plot fig = sankey_plot( - adata=adata, - source_annotation=source_annotation, - target_annotation=target_annotation, - source_color_map=source_color_map, - target_color_map=target_color_map, - sankey_font=sankey_font - ) + adata=adata, + source_annotation=source_annotation, + target_annotation=target_annotation, + source_color_map=source_color_map, + target_color_map=target_color_map, + sankey_font=sankey_font + ) # Customize the Sankey diagram layout + width_in_pixels = width_num * dpi + height_in_pixels = height_num * dpi + fig.update_layout( - width=width_in_pixels, # Specify the width in pixels - height=height_in_pixels # Specify the height in pixels - ) - - # Show the plot with the specified display options - print(fig) - - # Use output prefix to avoid conflicts - output_prefix = params.get("Output_File", "sankey") - image_path = f"{output_prefix}_diagram.png" - - pio.write_image( - fig, - image_path, - width=width_in_pixels, # Specify the width in pixels - height=height_in_pixels, - engine='kaleido', # Use the 'kaleido' engine for high DPI images - scale=scale + width=width_in_pixels, + height=height_in_pixels ) + + print("Sankey plot generated") + + # IMPORTANT: No show() calls — causes hang in Galaxy + # plt.show() - REMOVED + # fig.show() - REMOVED + + # Handle saving — always save to disk for Galaxy + if save_to_disk: + # Prepare results dictionary + results_dict = {} + + # Save Plotly HTML (the actual interactive Sankey diagram) + if "html" in params["outputs"]: + html_content = pio.to_html(fig, full_html=True, include_plotlyjs='cdn') + results_dict["html"] = {"sankey_plot": html_content} + print("Plotly HTML prepared for saving") + + if show_static_image: + # Generate a static matplotlib placeholder figure. + # Disabled by default on Galaxy because Plotly HTML-to-PNG + # conversion hangs in the Galaxy container environment. + # The interactive HTML is the first-class output. + print("Creating matplotlib figure...") + static_fig, ax = plt.subplots( + figsize=(width_num, height_num), dpi=dpi + ) + ax.text( + 0.5, 0.6, 'Sankey Diagram', + ha='center', va='center', transform=ax.transAxes, + fontsize=16, fontweight='bold' + ) + ax.text( + 0.5, 0.5, + f'{source_annotation} → {target_annotation}', + ha='center', va='center', transform=ax.transAxes, + fontsize=12 + ) + ax.text( + 0.5, 0.3, + 'View HTML output for interactive diagram', + ha='center', va='center', transform=ax.transAxes, + fontsize=10, style='italic' + ) + ax.axis('off') + ax.add_patch(plt.Rectangle( + (0.1, 0.2), 0.8, 0.5, + fill=False, edgecolor='gray', linewidth=1, + transform=ax.transAxes + )) + + if "figures" in params["outputs"]: + results_dict["figures"] = {"sankey_plot": static_fig} + print("Matplotlib figure prepared for saving") + + # Use centralized save_results function + print("Saving all results...") + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) - img = plt.imread(image_path) - static, axs = plt.subplots(1, 1, figsize=(width_num, height_num), dpi=dpi) - - # Load and display the image using Matplotlib - axs.imshow(img) - axs.axis('off') - if show_plot: - plt.show() + if show_static_image: + plt.close(static_fig) - if show_plot: - fig.show() + print(f"✓ Sankey Plot completed successfully") + print(f" Outputs saved: {list(saved_files.keys())}") - # Handle saving if requested - if save_results: - saved_files = {} - output_prefix = params.get("Output_File", "sankey") - - # Save the static plot - static_file = f"{output_prefix}_static.png" - static.savefig(static_file, dpi=dpi, bbox_inches='tight') - saved_files[static_file] = static_file - - # Save the interactive plot - interactive_file = f"{output_prefix}_interactive.html" - pio.write_html(fig, interactive_file) - saved_files[interactive_file] = interactive_file - - # Save the intermediate PNG that was created - saved_files[image_path] = image_path - - # Close figures after saving - plt.close(static) - - print(f"Sankey Plot completed → {list(saved_files.keys())}") return saved_files - - return None + else: + # For non-Galaxy use (testing) + print("Returning None (display mode not supported)") + return None # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python sankey_plot_template.py ", + "Usage: python sankey_plot_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) - - if isinstance(result, dict): - print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") - else: - print("\nPlots displayed (not saved)") \ No newline at end of file + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + print("\n" + "="*60) + print("SANKEY PLOT - GALAXY PRODUCTION VERSION") + print("="*60 + "\n") + + try: + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir, + save_to_disk=True # Always save for Galaxy + ) + + if isinstance(result, dict): + print("\nOutput files generated:") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") + + print("\n✓ SUCCESS - Job completed without hanging") + sys.exit(0) + + except Exception as e: + print(f"\n✗ ERROR: {e}", file=sys.stderr) + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/src/spac/templates/select_values_template.py b/src/spac/templates/select_values_template.py index 223105cd..e84723a4 100644 --- a/src/spac/templates/select_values_template.py +++ b/src/spac/templates/select_values_template.py @@ -2,6 +2,9 @@ Platform-agnostic Select Values template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.select_values_template import run_from_json @@ -10,24 +13,25 @@ import json import sys from pathlib import Path -from typing import Any, Dict, Union +from typing import Any, Dict, Union, Tuple import pandas as pd import warnings -import pickle +import logging # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) from spac.data_utils import select_values from spac.templates.template_utils import ( - save_outputs, + save_results, parse_params, ) def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], pd.DataFrame]: """ Execute Select Values analysis with parameters from JSON. @@ -36,46 +40,85 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the dataframe - directly for in-memory workflows. Default is True. + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Dataset": "path/to/dataframe.csv", + "Annotation_of_Interest": "cell_type", + "Label_s_of_Interest": ["T cells", "B cells"], + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If True, saves the filtered DataFrame + to a CSV file. If False, returns the DataFrame directly for in-memory + workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. All outputs will be saved relative to this directory. Returns ------- dict or DataFrame - If save_results=True: Dictionary of saved file paths - If save_results=False: The filtered DataFrame + If save_to_disk=True: Dictionary of saved file paths with structure: + { + "dataframe": "path/to/dataframe.csv" + } + If save_to_disk=False: The filtered DataFrame + + Notes + ----- + Output Structure: + - DataFrame is saved as a single CSV file + - When save_to_disk=False, the DataFrame is returned for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["dataframe"]) # Path to saved CSV file + + >>> # Get results in memory + >>> filtered_df = run_from_json("params.json", save_to_disk=False) + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") """ # Parse parameters from JSON params = parse_params(json_path) - # Load upstream data - could be DataFrame, CSV, or pickle + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + # DataFrames typically use file type per standardized schema + if "outputs" not in params: + params["outputs"] = { + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + + # Load upstream data - could be DataFrame, CSV upstream_dataset = params["Upstream_Dataset"] if isinstance(upstream_dataset, pd.DataFrame): input_dataset = upstream_dataset # Direct DataFrame from previous step elif isinstance(upstream_dataset, (str, Path)): - path = Path(upstream_dataset) - if path.suffix.lower() == '.csv': - input_dataset = pd.read_csv(path) - elif path.suffix.lower() in ['.pickle', '.pkl', '.p']: - with open(path, 'rb') as f: - input_dataset = pickle.load(f) - else: - raise ValueError( - f"Unsupported file format: {path.suffix}. " - f"Supported formats: .csv, .pickle, .pkl, .p" - ) + try: + input_dataset = pd.read_csv(upstream_dataset) + except Exception as e: + raise ValueError(f"Failed to read CSV from {upstream_dataset}: {e}") else: raise TypeError( f"Upstream_Dataset must be DataFrame or file path. " f"Got {type(upstream_dataset)}" ) - # Extract parameters - observation = params["Annotation_of_Interest"] - values = params["Label_s_of_Interest"] + # Extract parameters - support both "Label_s_of_Interest" and "Labels_of_Interest" + # for backward compatibility with JSON template + observation = params.get("Annotation_of_Interest") + values = params.get("Label_s_of_Interest") or params.get("Labels_of_Interest") with warnings.catch_warnings(record=True) as caught_warnings: warnings.simplefilter("always") @@ -95,36 +138,67 @@ def run_from_json( if hasattr(warning, 'message'): raise ValueError(str(warning.message)) - print(filtered_dataset.info()) + logging.info(filtered_dataset.info()) - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "select_values.csv") - saved_files = save_outputs({output_file: filtered_dataset}) + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + # Check for dataframe output + if "dataframe" in params["outputs"]: + results_dict["dataframe"] = filtered_dataset + + # Use centralized save_results function + # All file handling and logging is now done by save_results + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) - print(f"Select Values completed → {saved_files[output_file]}") + logging.info("Select Values analysis completed successfully.") return saved_files else: # Return the dataframe directly for in-memory workflows - print("Returning DataFrame (not saving to file)") + logging.info("Returning DataFrame for in-memory use") return filtered_dataset # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python select_values_template.py ", + "Usage: python select_values_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + # Display results based on return type if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nReturned DataFrame") \ No newline at end of file + print("\nReturned DataFrame") + print(f"DataFrame shape: {result.shape}") diff --git a/src/spac/templates/setup_analysis_template.py b/src/spac/templates/setup_analysis_template.py index 298e6d32..8bc8100b 100644 --- a/src/spac/templates/setup_analysis_template.py +++ b/src/spac/templates/setup_analysis_template.py @@ -2,6 +2,9 @@ Platform-agnostic Setup Analysis template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Follows standardized output schema where analysis is saved as a file. + Usage ----- >>> from spac.templates.setup_analysis_template import run_from_json @@ -21,7 +24,7 @@ from spac.data_utils import ingest_cells from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, text_to_value ) @@ -29,7 +32,8 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], Any]: """ Execute Setup Analysis with parameters from JSON. @@ -38,20 +42,69 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the adata object - directly for in-memory workflows. Default is True. + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Dataset": "path/to/data.csv", + "Features_to_Analyze": ["CD25", "CD3D"], + "Feature_Regex": [], + "X_Coordinate_Column": "X_centroid", + "Y_Coordinate_Column": "Y_centroid", + "Annotation_s_": ["cell_type"], + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If True, saves the AnnData object + to a pickle file. If False, returns the AnnData object directly + for in-memory workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. All outputs will be saved relative to this directory. Returns ------- dict or AnnData - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed AnnData object + If save_to_disk=True: Dictionary of saved file paths with structure: + {"analysis": "path/to/output.pickle"} + If save_to_disk=False: The processed AnnData object for in-memory use + + Notes + ----- + Output Structure: + - Analysis output is saved as a single pickle file (standardized for analysis outputs) + - When save_to_disk=False, the AnnData object is returned for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["analysis"]) # Path to saved pickle file + >>> # './output.pickle' + + >>> # Get results in memory for further processing + >>> adata = run_from_json("params.json", save_to_disk=False) + >>> # Can now work with adata object directly + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") """ # Parse parameters from JSON params = parse_params(json_path) + # Ensure outputs configuration exists with standardized defaults + # Analysis uses file type per standardized schema + if "outputs" not in params: + # Get output filename from params or use default + output_file = params.get("Output_File", "output.pickle") + if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): + output_file = output_file + '.pickle' + + params["outputs"] = { + "analysis": {"type": "file", "name": output_file} + } + # Extract parameters upstream_dataset = params["Upstream_Dataset"] feature_names = params["Features_to_Analyze"] @@ -62,13 +115,13 @@ def run_from_json( # Load upstream data - could be DataFrame or CSV if isinstance(upstream_dataset, (str, Path)): - # Check if it's a pickle/h5ad file or CSV - path = Path(upstream_dataset) - if path.suffix.lower() in ['.pickle', '.pkl', '.p', '.h5ad']: - input_dataset = load_input(upstream_dataset) - else: - # Assume it's a CSV file + try: input_dataset = pd.read_csv(upstream_dataset) + # Validate it's a proper DataFrame + if input_dataset.empty: + raise ValueError("CSV file is empty") + except Exception as e: + raise ValueError(f"Failed to read CSV from {upstream_dataset}: {e}") else: # Already a DataFrame input_dataset = upstream_dataset @@ -114,23 +167,29 @@ def run_from_json( annotation=annotation ) - print("Analysis Setup:") - print(ingested_anndata) - print("Schema:") - print(ingested_anndata.var_names) - - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "transform_output.pickle") - # Default to pickle format if no recognized extension - if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): - output_file = output_file + '.pickle' - - saved_files = save_outputs({output_file: ingested_anndata}) + logging.info("Analysis Setup:") + logging.info(f"{ingested_anndata}") + logging.info("Schema:") + logging.info(f"{ingested_anndata.var_names.tolist()}") + + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + if "analysis" in params["outputs"]: + results_dict["analysis"] = ingested_anndata + + # Use centralized save_results function + # All file handling and logging is now done by save_results + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) logging.info( - f"Setup Analysis completed → {saved_files[output_file]}" + f"Setup Analysis completed → {saved_files['analysis']}" ) return saved_files else: @@ -141,18 +200,34 @@ def run_from_json( # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( "Usage: python setup_analysis_template.py ", file=sys.stderr ) sys.exit(1) - saved_files = run_from_json(sys.argv[1]) - - if isinstance(saved_files, dict): + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + + # Display results based on return type + if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in saved_files.items(): - print(f" {filename}: {filepath}") + for key, path in result.items(): + print(f" {key}: {path}") else: - print("\nReturned AnnData object") \ No newline at end of file + print("\nReturned AnnData object for in-memory use") + print(f"AnnData: {result}") + print(f"Shape: {result.shape}") \ No newline at end of file diff --git a/src/spac/templates/spatial_interaction_template.py b/src/spac/templates/spatial_interaction_template.py index 85e5fc88..bee16a41 100644 --- a/src/spac/templates/spatial_interaction_template.py +++ b/src/spac/templates/spatial_interaction_template.py @@ -2,6 +2,9 @@ Platform-agnostic Spatial Interaction template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Follows standardized output schema where figures are saved as directories. + Usage ----- >>> from spac.templates.spatial_interaction_template import run_from_json @@ -10,7 +13,7 @@ import json import sys from pathlib import Path -from typing import Any, Dict, Union, List, Optional +from typing import Any, Dict, Union, List, Optional, Tuple import pandas as pd import numpy as np from PIL import Image @@ -23,7 +26,7 @@ from spac.spatial_analysis import spatial_interaction from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, text_to_value, ) @@ -31,9 +34,10 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True, - show_plot: bool = True -) -> Union[Dict[str, str], None]: + save_to_disk: bool = True, + show_plot: bool = True, + output_dir: str = None, +) -> Union[Dict[str, Union[str, List[str]]], Tuple[List[Any], Dict[str, pd.DataFrame]]]: """ Execute Spatial Interaction analysis with parameters from JSON. Replicates the NIDAP template functionality exactly. @@ -41,19 +45,30 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. Default is True. + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Annotation": "cell_type", + "Spatial_Analysis_Method": "Neighborhood Enrichment", + "outputs": { + "figures": {"type": "directory", "name": "figures_dir"}, + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If True, saves figures to a directory + and matrices to CSV files using centralized save_results. Default is True. show_plot : bool, optional Whether to display the plot. Default is True. + output_dir : str or Path, optional + Base directory for outputs. If None, uses params['Output_Directory'] or '.' Returns ------- - dict or None - If save_results=True: Dictionary of saved file paths containing: - - PNG files: Heatmap visualizations of spatial interactions - - CSV files: Matrices with interaction scores/counts between cell types - If save_results=False: None (plots are displayed only) + dict or tuple + If save_to_disk=True: Dictionary mapping output types to saved file paths + If save_to_disk=False: Tuple of (figures_list, matrices_dict) for in-memory use """ # Parse parameters from JSON params = parse_params(json_path) @@ -95,7 +110,9 @@ def save_matrix(matrix): print(file_name) print(data_df) # In SPAC, collect matrices for later saving instead of - # direct file write + # direct file write. Store them with proper extension if missing. + if not file_name.endswith('.csv'): + file_name = f"{file_name}.csv" matrices[file_name] = data_df def update_nidap_display( @@ -218,56 +235,90 @@ def update_nidap_display( save_matrix(matrix) # Handle saving if requested (separate from NIDAP logic) - if save_results and (figures or matrices): - saved_files = {} - output_prefix = params.get("Output_File", "spatial_interaction") + if save_to_disk: + # Ensure outputs configuration exists + if "outputs" not in params: + # Provide default outputs config if not present + params["outputs"] = { + "figures": {"type": "directory", "name": "figures"}, + "dataframes": {"type": "directory", "name": "matrices"} + } + + # Prepare results dictionary + results_dict = {} - # Save figures + # Package figures in a dictionary for directory saving if figures: - if len(figures) == 1: - output_file = f"{output_prefix}.png" - figures[0].savefig( - output_file, dpi=dpi, bbox_inches='tight') - saved_files[output_file] = output_file - else: - for i, fig in enumerate(figures): - output_file = f"{output_prefix}_plot_{i+1}.png" - fig.savefig( - output_file, dpi=dpi, bbox_inches='tight') - saved_files[output_file] = output_file + # Store figures with meaningful names + figures_dict = {} + for i, fig in enumerate(figures): + # Extract title if available for better naming + try: + ax = fig.axes[0] if fig.axes else None + title = ax.get_title() if ax and ax.get_title() else f"interaction_plot_{i+1}" + # Clean title for filename + title = title.replace(" ", "_").replace("/", "_").replace(":", "") + figures_dict[f"{title}.png"] = fig + except: + figures_dict[f"interaction_plot_{i+1}.png"] = fig + + results_dict["figures"] = figures_dict + + # Add matrices (already have .csv extension added) + if matrices: + results_dict["dataframes"] = matrices - # Save matrices - for file_name, df in matrices.items(): - saved_files.update(save_outputs({file_name: df})) + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) - # Close figures after saving + # Close figures after saving to free memory for fig in figures: plt.close(fig) print( - f"Spatial Interaction completed → " + f"Spatial Interaction completed -> " f"{list(saved_files.keys())}" ) return saved_files - - return None + else: + # Return objects directly for in-memory workflows + return figures, matrices # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python spatial_interaction_template.py " - "", + "Usage: python spatial_interaction_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + # Display results based on return type if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nReturned data object") \ No newline at end of file + figures_list, matrices_dict = result + print("\nReturned figures and matrices for in-memory use") + print(f"Number of figures: {len(figures_list)}") + print(f"Number of matrices: {len(matrices_dict)}") diff --git a/src/spac/templates/spatial_plot_template.py b/src/spac/templates/spatial_plot_template.py index 28cfb168..deb93239 100644 --- a/src/spac/templates/spatial_plot_template.py +++ b/src/spac/templates/spatial_plot_template.py @@ -2,6 +2,9 @@ Platform-agnostic Spatial Plot template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.spatial_plot_template import run_from_json @@ -13,6 +16,7 @@ from typing import Any, Dict, Union, List, Optional import matplotlib.pyplot as plt from functools import partial +import logging # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) @@ -22,6 +26,7 @@ from spac.utils import check_annotation from spac.templates.template_utils import ( load_input, + save_results, parse_params, text_to_value, ) @@ -29,9 +34,10 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True, - show_plots: bool = True -) -> Union[Dict[str, str], None]: + save_to_disk: bool = True, + show_plots: bool = True, + output_dir: str = None, +) -> Union[Dict[str, Union[str, List[str]]], List[plt.Figure]]: """ Execute Spatial Plot analysis with parameters from JSON. Replicates the NIDAP template functionality exactly. @@ -39,51 +45,81 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns None. - Default is True. + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Stratify": true, + "Stratify_By": ["slide_id"], + "Color_By": "Annotation", + ... + "outputs": { + "figures": {"type": "directory", "name": "figures_dir"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If False, returns the figures + directly for in-memory workflows. Default is True. show_plots : bool, optional Whether to display the plots. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. Returns ------- - dict or None - If save_results=True: Dictionary of saved file paths - If save_results=False: None + dict or list + If save_to_disk=True: Dictionary of saved file paths + If save_to_disk=False: List of matplotlib figures """ + # Set up logging + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + # Parse parameters from JSON params = parse_params(json_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + if "outputs" not in params: + params["outputs"] = { + "figures": {"type": "directory", "name": "figures_dir"} + } + # Load the upstream analysis data adata = load_input(params["Upstream_Analysis"]) # Extract parameters exactly as in NIDAP template - annotation = params["Annotation_to_Highlight"] - feature = params["Feature_to_Highlight"] - layer = params["Table"] + annotation = params.get("Annotation_to_Highlight", "None") + feature = params.get("Feature_to_Highlight", "") + layer = params.get("Table", "Original") - alpha = params["Dot_Transparency"] - spot_size = params["Dot_Size"] - image_height = params["Figure_Height"] - image_width = params["Figure_Width"] - dpi = params["Figure_DPI"] - font_size = params["Font_Size"] - vmin = params["Lower_Colorbar_Bound"] - vmax = params["Upper_Colorbar_Bound"] - color_by = params["Color_By"] - stratify = params["Stratify"] - stratify_by = params["Stratify_By"] + alpha = params.get("Dot_Transparency", 0.5) + spot_size = params.get("Dot_Size", 25) + image_height = params.get("Figure_Height", 6) + image_width = params.get("Figure_Width", 12) + dpi = params.get("Figure_DPI", 200) + font_size = params.get("Font_Size", 12) + vmin = params.get("Lower_Colorbar_Bound", 999) + vmax = params.get("Upper_Colorbar_Bound", -999) + color_by = params.get("Color_By", "Annotation") + stratify = params.get("Stratify", True) + stratify_by = params.get("Stratify_By", []) if stratify and len(stratify_by) == 0: raise ValueError( 'Please set at least one annotation in the "Stratify By" ' 'option, or set the "Stratify" to False.' ) - check_annotation( - adata, - annotations=stratify_by - ) + + if stratify: + check_annotation( + adata, + annotations=stratify_by + ) # Process feature and annotation with text_to_value feature = text_to_value(feature) @@ -107,8 +143,8 @@ def run_from_json( layer=layer ) - # Track figures for optional saving - figures = [] + # Track figures for saving + figures_dict = {} if not stratify: plt.rcParams['font.size'] = font_size @@ -124,7 +160,7 @@ def run_from_json( title = f'Table:"{layer}" \n Feature:"{feature}"' ax[0].set_title(title) - figures.append(fig) + figures_dict["spatial_plot"] = fig if show_plots: plt.show() @@ -137,16 +173,16 @@ def run_from_json( unique_values = adata.obs[combined_label].unique() - print(unique_values) + logger.info(f"Unique stratification values: {unique_values}") max_length = min(len(unique_values), 20) if len(unique_values) > 20: - print( - f'WARNING: There are "{len(unique_values)}" unique plots, ' + logger.warning( + f'There are "{len(unique_values)}" unique plots, ' 'displaying only the first 20 plots.' ) - for value in unique_values[:max_length]: + for idx, value in enumerate(unique_values[:max_length]): filtered_adata = select_values( data=adata, annotation=combined_label, values=value ) @@ -164,48 +200,72 @@ def run_from_json( title = f'{title}\n Stratify by: {value}' ax[0].set_title(title) - figures.append(fig) + # Use sanitized value for figure name + safe_value = str(value).replace('/', '_').replace('\\', '_') + figures_dict[f"spatial_plot_{safe_value}"] = fig if show_plots: plt.show() - # Handle saving if requested (separate from NIDAP logic) - if save_results and figures: - saved_files = {} - output_prefix = params.get("Output_File", "spatial_plot") - - if len(figures) == 1: - output_file = f"{output_prefix}.png" - figures[0].savefig(output_file, dpi=dpi, bbox_inches='tight') - saved_files[output_file] = output_file - else: - for i, fig in enumerate(figures): - output_file = f"{output_prefix}_plot_{i+1}.png" - fig.savefig(output_file, dpi=dpi, bbox_inches='tight') - saved_files[output_file] = output_file - + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + # Check for figures output + if "figures" in params["outputs"]: + results_dict["figures"] = figures_dict + + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + # Close figures after saving - for fig in figures: + for fig in figures_dict.values(): plt.close(fig) - - print(f"Spatial Plot completed → {list(saved_files.keys())}") - return saved_files - return None + logger.info("Spatial Plot analysis completed successfully.") + return saved_files + else: + # Return the figures directly for in-memory workflows + logger.info("Returning figures for in-memory use") + return list(figures_dict.values()) # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python spatial_plot_template.py ", + "Usage: python spatial_plot_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) - - if result: + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + + if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") \ No newline at end of file + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") + else: + print(f"\nReturned {len(result)} figures") diff --git a/src/spac/templates/subset_analysis_template.py b/src/spac/templates/subset_analysis_template.py index 915501d2..e32286de 100644 --- a/src/spac/templates/subset_analysis_template.py +++ b/src/spac/templates/subset_analysis_template.py @@ -2,6 +2,9 @@ Platform-agnostic Subset Analysis template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.subset_analysis_template import run_from_json @@ -10,9 +13,10 @@ import json import sys from pathlib import Path -from typing import Any, Dict, Union, List +from typing import Any, Dict, Union, List, Tuple import pandas as pd import warnings +import logging # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) @@ -21,7 +25,7 @@ from spac.data_utils import select_values from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, text_to_value, ) @@ -29,7 +33,8 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], Any]: """ Execute Subset Analysis with parameters from JSON. @@ -38,20 +43,66 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the adata object - directly for in-memory workflows. Default is True. + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Annotation_of_interest": "cell_type", + "Labels": ["T cells", "B cells"], + "Include_Exclude": "Include Selected Labels", + "outputs": { + "analysis": {"type": "file", "name": "transform_output.pickle"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If True, saves the filtered AnnData + to a pickle file. If False, returns the AnnData object directly for + in-memory workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. All outputs will be saved relative to this directory. Returns ------- dict or AnnData - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed AnnData object + If save_to_disk=True: Dictionary of saved file paths with structure: + { + "analysis": "path/to/transform_output.pickle" + } + If save_to_disk=False: The processed AnnData object + + Notes + ----- + Output Structure: + - Analysis output is saved as a single pickle file + - When save_to_disk=False, the AnnData object is returned for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["analysis"]) # Path to saved pickle file + + >>> # Get results in memory + >>> filtered_adata = run_from_json("params.json", save_to_disk=False) + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") """ # Parse parameters from JSON params = parse_params(json_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + # Analysis outputs use file type per standardized schema + if "outputs" not in params: + params["outputs"] = { + "analysis": {"type": "file", "name": "transform_output.pickle"} + } + # Load the upstream analysis data adata = load_input(params["Upstream_Analysis"]) @@ -71,63 +122,98 @@ def run_from_json( values_to_exclude = labels with warnings.catch_warnings(record=True) as caught_warnings: + warnings.simplefilter("always") filtered_adata = select_values( data=adata, annotation=annotation, values=values_to_include, exclude_values=values_to_exclude ) + # Only process warnings that are relevant to the select_values operation if caught_warnings: for warning in caught_warnings: - raise ValueError(warning.message) + # Skip deprecation warnings from numpy/pandas + if (hasattr(warning, 'category') and + issubclass(warning.category, DeprecationWarning)): + continue + # Raise actual operational warnings as errors + if hasattr(warning, 'message'): + raise ValueError(str(warning.message)) - print(filtered_adata) - print("\n") + logging.info(filtered_adata) + logging.info("\n") # Count and display occurrences of each label in the annotation label_counts = filtered_adata.obs[annotation].value_counts() - print(label_counts) - print("\n") + logging.info(label_counts) + logging.info("\n") dataframe = pd.DataFrame( filtered_adata.X, columns=filtered_adata.var.index, index=filtered_adata.obs.index ) - print(dataframe.describe()) + logging.info(dataframe.describe()) - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "transform_output.pickle") - # Default to pickle format if no recognized extension - if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): - output_file = output_file + '.pickle' + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} - saved_files = save_outputs({output_file: filtered_adata}) + # Check for analysis output (backward compatibility with "Output_File") + if "analysis" in params["outputs"]: + results_dict["analysis"] = filtered_adata - print(f"Subset Analysis completed → {saved_files[output_file]}") + # Use centralized save_results function + # All file handling and logging is now done by save_results + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + logging.info("Subset Analysis completed successfully.") return saved_files else: # Return the adata object directly for in-memory workflows - print("Returning AnnData object (not saving to file)") + logging.info("Returning AnnData object for in-memory use") return filtered_adata # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python subset_analysis_template.py ", + "Usage: python subset_analysis_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + # Display results based on return type if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nReturned AnnData object") \ No newline at end of file + print("\nReturned AnnData object") + print(f"AnnData shape: {result.shape}") diff --git a/src/spac/templates/summarize_annotation_statistics_template.py b/src/spac/templates/summarize_annotation_statistics_template.py index cd7c4ff3..04557b35 100644 --- a/src/spac/templates/summarize_annotation_statistics_template.py +++ b/src/spac/templates/summarize_annotation_statistics_template.py @@ -2,6 +2,9 @@ Platform-agnostic Summarize Annotation's Statistics template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.summarize_annotation_statistics_template import \ @@ -10,8 +13,9 @@ """ import json import sys +import logging from pathlib import Path -from typing import Any, Dict, Union, List, Optional, Tuple +from typing import Any, Dict, Union, List, Optional import pandas as pd # Add parent directory to path for imports @@ -20,15 +24,18 @@ from spac.transformations import get_cluster_info from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, text_to_value, ) +logger = logging.getLogger(__name__) + def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], pd.DataFrame]: """ Execute Summarize Annotation's Statistics analysis with parameters from @@ -37,20 +44,50 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the dataframe + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Table_to_Process": "Original", + "Annotation": "phenotype", + "Feature_s_": ["All"], + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If False, returns the dataframe directly for in-memory workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. Returns ------- dict or DataFrame - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed DataFrame + If save_to_disk=True: Dictionary of saved file paths with structure: + {"dataframe": "path/to/dataframe.csv"} + If save_to_disk=False: The processed DataFrame + + Notes + ----- + Output Structure: + - DataFrame is saved as a single CSV file + - When save_to_disk=False, the DataFrame is returned for programmatic use """ # Parse parameters from JSON params = parse_params(json_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + if "outputs" not in params: + params["outputs"] = { + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + # Load the upstream analysis data adata = load_input(params["Upstream_Analysis"]) @@ -61,7 +98,7 @@ def run_from_json( if layer == "Original": layer = None - + if len(features) == 1 and features[0] == "All": features = None @@ -78,46 +115,71 @@ def run_from_json( df = pd.DataFrame(info) # Renaming columns to avoid spaces and special characters - # Assuming `info` is a pandas DataFrame df.columns = [ col.replace(" ", "_").replace("-", "_") for col in df.columns ] # Get summary statistics of returned dataset - print("Summary statistics of the dataset:", df.describe()) - - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "annotation_summaries.csv") - saved_files = save_outputs({output_file: df}) - - print( - f"Summarize Annotation's Statistics completed → " - f"{saved_files[output_file]}" + logger.info(f"Summary statistics of the dataset:\n{df.describe()}") + + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + if "dataframe" in params["outputs"]: + results_dict["dataframe"] = df + + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + logger.info( + "Summarize Annotation's Statistics analysis completed successfully." ) return saved_files else: # Return the dataframe directly for in-memory workflows - print("Returning DataFrame (not saving to file)") + logger.info("Returning DataFrame for in-memory use") return df # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( "Usage: python summarize_annotation_statistics_template.py " - "", + " [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nReturned DataFrame") \ No newline at end of file + print("\nReturned DataFrame") + print(f"DataFrame shape: {result.shape}") diff --git a/src/spac/templates/summarize_dataframe_template.py b/src/spac/templates/summarize_dataframe_template.py index 20669876..92a43e0d 100644 --- a/src/spac/templates/summarize_dataframe_template.py +++ b/src/spac/templates/summarize_dataframe_template.py @@ -2,6 +2,9 @@ Platform-agnostic Summarize DataFrame template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.summarize_dataframe_template import run_from_json @@ -12,6 +15,7 @@ from pathlib import Path from typing import Any, Dict, Union, List, Optional, Tuple import pandas as pd +import logging # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent.parent.parent)) @@ -19,16 +23,16 @@ from spac.data_utils import summarize_dataframe from spac.visualization import present_summary_as_figure from spac.templates.template_utils import ( - load_input, - save_outputs, + save_results, parse_params, ) def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True, - show_plot: bool = True + save_to_disk: bool = True, + output_dir: str = None, + show_plot: bool = False, ) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: """ Execute Summarize DataFrame analysis with parameters from JSON. @@ -37,30 +41,90 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the figure and - dataframe directly for in-memory workflows. Default is True. + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Dataset": "path/to/dataframe.csv", + "Columns": ["col1", "col2"], + "Print_Missing_Location": false, + "outputs": { + "html": {"type": "directory", "name": "html_dir"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If True, saves the HTML summary + to a directory. If False, returns the figure and dataframe directly + for in-memory workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. All outputs will be saved relative to this directory. show_plot : bool, optional - Whether to display the plot. Default is True. + Whether to display the plot interactively. Default is False. Returns ------- dict or tuple - If save_results=True: Dictionary of saved file paths - If save_results=False: Tuple of (figure, dataframe) + If save_to_disk=True: Dictionary of saved file paths with structure: + { + "html": ["path/to/html_dir/summary.html"] + } + If save_to_disk=False: Tuple of (figure, summary_dataframe) + + Notes + ----- + Output Structure: + - HTML is saved to a directory as specified in outputs config + - When save_to_disk=False, returns (figure, summary_df) for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["html"]) # List of paths to saved HTML files + + >>> # Get results in memory + >>> fig, summary_df = run_from_json("params.json", save_to_disk=False) + + >>> # Custom output directory with interactive display + >>> saved = run_from_json("params.json", output_dir="/custom/path", show_plot=True) """ # Parse parameters from JSON params = parse_params(json_path) - # Load the upstream analysis data - # Note: load_input is for AnnData objects; DataFrames from CSV are handled separately - input_path = params["Calculate_Centroids"] - if isinstance(input_path, str) and input_path.endswith('.csv'): - df = pd.read_csv(input_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + # HTML outputs use directory type per standardized schema + if "outputs" not in params: + params["outputs"] = { + "html": {"type": "directory", "name": "html_dir"} + } + + # Load upstream data - DataFrame or CSV file + # Corrected "Calculate_Centroids" to "Upstream_Dataset" in the blueprint + input_path = params.get("Upstream_Dataset") + if isinstance(input_path, pd.DataFrame): + df = input_path # Direct DataFrame from previous step + elif isinstance(input_path, (str, Path)): + # Galaxy passes .dat files, but they contain CSV data + # Don't check extension - directly read as CSV + path = Path(input_path) + try: + df = pd.read_csv(path) + logging.info(f"Successfully loaded CSV data from: {path}") + except Exception as e: + raise ValueError( + f"Failed to read CSV data from '{path}'. " + f"This tool expects CSV/tabular format. " + f"Error: {str(e)}" + ) else: - # For pickle files that contain DataFrames - df = load_input(input_path) + raise TypeError( + f"Input dataset must be DataFrame or file path. " + f"Got {type(input_path)}" + ) # Extract parameters columns = params["Columns"] @@ -70,48 +134,74 @@ def run_from_json( summary = summarize_dataframe( df, columns=columns, - print_nan_locations=print_missing_location) + print_nan_locations=print_missing_location + ) - # Generate HTML from the summary. + # Generate figure from the summary fig = present_summary_as_figure(summary) if show_plot: fig.show() # Opens in an interactive Plotly window - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "summary.html") + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} - # Since the figure is a Plotly figure, we save it as HTML - if not output_file.endswith('.html'): - output_file = output_file + '.html' + # Check for html output - convert figure to HTML string + if "html" in params["outputs"]: + # Convert Plotly figure to HTML string for save_results + html_content = fig.to_html(full_html=True, include_plotlyjs='cdn') + results_dict["html"] = {"summary": html_content} - fig.write_html(output_file) - saved_files = {output_file: output_file} + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) - print(f"Summarize DataFrame completed → {saved_files[output_file]}") + logging.info("Summarize DataFrame analysis completed successfully.") return saved_files else: # Return the figure and summary dataframe directly for in-memory workflows - print("Returning figure and dataframe (not saving to file)") + logging.info("Returning figure and dataframe for in-memory use") return fig, summary # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python summarize_dataframe_template.py ", + "Usage: python summarize_dataframe_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + # Display results based on return type if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nReturned figure and dataframe") \ No newline at end of file + print("\nReturned figure and dataframe") diff --git a/src/spac/templates/template_utils.py b/src/spac/templates/template_utils.py index 48d45abf..7c7e9872 100644 --- a/src/spac/templates/template_utils.py +++ b/src/spac/templates/template_utils.py @@ -6,6 +6,8 @@ import anndata as ad import re import logging +import matplotlib.pyplot as plt + logger = logging.getLogger(__name__) @@ -33,7 +35,6 @@ def load_input(file_path: Union[str, Path]): if suffix in ['.h5ad', '.h5']: # Load h5ad file try: - import anndata as ad return ad.read_h5ad(path) except ImportError: raise ImportError( @@ -51,7 +52,6 @@ def load_input(file_path: Union[str, Path]): # Try to detect file type by content try: # First try h5ad - import anndata as ad return ad.read_h5ad(path) except Exception: # Fall back to pickle @@ -65,77 +65,254 @@ def load_input(file_path: Union[str, Path]): ) -def save_outputs( - outputs: Dict[str, Any], - output_dir: Union[str, Path] = "." -) -> Dict[str, str]: +def save_results( + results: Dict[str, Any], + params: Dict[str, Any], + output_base_dir: Union[str, Path] = None +) -> Dict[str, Union[str, List[str]]]: """ - Save multiple outputs to files and return a dict {filename: absolute_path}. - (Always a dict, even if just one file.) - + Save results based on output configuration in params. + + This function reads the output configuration from the params dictionary + and saves results accordingly. It applies a standardized schema where: + - figures → directory (may contain one or many) + - analysis → file + - dataframe → file (or directory for exceptions like "Neighborhood Profile") + - html → directory + Parameters ---------- - outputs : dict - Dictionary where: - - key: filename (with extension) - - value: object to save - output_dir : str or Path - Directory to save files - + results : dict + Dictionary of results to save where: + - key: result type ("analysis", "dataframes", "figures", "html") + - value: object(s) to save (single object, list, or dict of objects) + params : dict + Parameters dict containing 'outputs' configuration with structure: + { + "outputs": { + "figures": {"type": "directory", "name": "figures_dir"}, + "html": {"type": "directory", "name": "html_dir"}, + "dataframe": {"type": "file", "name": "dataframe.csv"}, + "analysis": {"type": "file", "name": "output.pickle"} + } + } + output_base_dir : str or Path, optional + Base directory for outputs. If None, uses params['Output_Directory'] or '.' + Returns ------- dict - Dictionary of saved file paths - + Dictionary mapping output types to saved file paths: + - For files: string path + - For directories: list of string paths + Example ------- - >>> outputs = { - ... "adata.pickle": adata, # Preferred format - ... "results.csv": results_df, - ... "adata.h5ad": adata # Still supported + >>> params = { + ... "outputs": { + ... "figures": {"type": "directory", "name": "figure_outputs"}, + ... "dataframe": {"type": "file", "name": "summary.csv"} + ... } ... } - >>> saved = save_outputs(outputs, "results/") + >>> results = {"figures": {"boxplot": fig}, "dataframe": df} + >>> saved = save_results(results, params) """ - output_dir = Path(output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - + # Get output directory from params if not provided + if output_base_dir is None: + output_base_dir = params.get("Output_Directory", ".") + output_base_dir = Path(output_base_dir) + + # Get outputs config from params + outputs_config = params.get("outputs", {}) + if not outputs_config: + logger.warning("No outputs configuration found in params") + return {} + saved_files = {} - - for filename, obj in outputs.items(): - filepath = output_dir / filename - - # Save based on file extension - if filename.endswith('.csv'): - obj.to_csv(filepath, index=False) - elif filename.endswith('.h5ad'): - # Still support h5ad, but not the default - if type(obj) is not ad.AnnData: - raise TypeError( - f"Object for '{str(filename)}' must be AnnData, " - f"got {type(obj)}" - ) - logger.info(f"Saving AnnData to {str(filepath)}") - logger.debug(f"AnnData object: {obj}") - obj.write_h5ad(str(filepath)) - logger.info(f"Saved AnnData to {str(filepath)}") - elif filename.endswith(('.pickle', '.pkl', '.p')): - with open(filepath, 'wb') as f: - pickle.dump(obj, f) - elif hasattr(obj, "savefig"): - obj.savefig(filepath.with_suffix('.png')) - filepath = filepath.with_suffix('.png') + + # Process each result based on configuration + for result_key, data in results.items(): + # Find matching config (case-insensitive match) + config = None + config_key = None + + for key, value in outputs_config.items(): + if key.lower() == result_key.lower(): + config = value + config_key = key + break + + if not config: + logger.warning(f"No output config for '{result_key}', skipping") + continue + + # Determine output type and name + output_type = config.get("type") + output_name = config.get("name", result_key) + + # Apply standardized schema if type not explicitly specified + if not output_type: + result_key_lower = result_key.lower() + if "figures" in result_key_lower: + output_type = "directory" + elif "analysis" in result_key_lower: + output_type = "file" + elif "dataframe" in result_key_lower: + # Special case: Neighborhood Profile gets directory treatment + if "neighborhood" in output_name.lower() and "profile" in output_name.lower(): + output_type = "directory" + else: + output_type = "file" + elif "html" in result_key_lower: + output_type = "directory" + else: + # Default based on data structure + output_type = "directory" if isinstance(data, (dict, list)) else "file" + + logger.debug(f"Auto-determined type '{output_type}' for '{result_key}'") + + # Save based on determined type + if output_type == "directory": + # Create directory and save multiple files + output_dir = output_base_dir / output_name + output_dir.mkdir(parents=True, exist_ok=True) + saved_files[config_key or result_key] = [] + + if isinstance(data, dict): + # Dictionary of named items + for name, obj in data.items(): + filepath = _save_single_object(obj, name, output_dir) + saved_files[config_key or result_key].append(str(filepath)) + + elif isinstance(data, (list, tuple)): + # List of items - auto-name them + for idx, obj in enumerate(data): + name = f"{result_key}_{idx}" + filepath = _save_single_object(obj, name, output_dir) + saved_files[config_key or result_key].append(str(filepath)) + + else: + # Single item saved to directory + filepath = _save_single_object(data, result_key, output_dir) + saved_files[config_key or result_key] = [str(filepath)] + + elif output_type == "file": + # Save as single file + output_path = output_base_dir / output_name + output_path.parent.mkdir(parents=True, exist_ok=True) + + # Handle different file types based on extension + if output_name.endswith('.pickle'): + with open(output_path, 'wb') as f: + pickle.dump(data, f) + + elif output_name.endswith('.csv'): + if isinstance(data, pd.DataFrame): + data.to_csv(output_path, index=False) + else: + # Convert to DataFrame if possible + df = pd.DataFrame(data) + df.to_csv(output_path, index=False) + + elif output_name.endswith('.h5ad'): + if hasattr(data, 'write_h5ad'): + data.write_h5ad(str(output_path)) + + elif output_name.endswith('.html'): + with open(output_path, 'w') as f: + f.write(str(data)) + + elif output_name.endswith(('.png', '.pdf', '.svg')): + if hasattr(data, 'savefig'): + data.savefig(output_path, dpi=300, bbox_inches='tight') + plt.close(data) # Close figure to free memory + + else: + # Default to pickle for unknown types + if not output_name.endswith('.pickle'): + output_path = output_path.with_suffix('.pickle') + with open(output_path, 'wb') as f: + pickle.dump(data, f) + + saved_files[config_key or result_key] = str(output_path) + + # Log summary of saved files + logger.info(f"Results saved to {output_base_dir}:") + for key, paths in saved_files.items(): + if isinstance(paths, list): + output_name = outputs_config.get(key, {}).get('name', key) + logger.info(f" {key}: {len(paths)} files in {output_base_dir}/{output_name}/") + for path in paths[:3]: # Show first 3 files + logger.debug(f" - {Path(path).name}") + if len(paths) > 3: + logger.debug(f" ... and {len(paths) - 3} more files") else: - # Default to pickle - filepath = filepath.with_suffix('.pickle') - with open(filepath, 'wb') as f: - pickle.dump(obj, f) + logger.info(f" {key}: {Path(paths).name}") + + return saved_files - print(type(filepath)) - print(type(filename)) - saved_files[str(filename)] = str(filepath) - print(f"Saved: {filepath}") - return saved_files +def _save_single_object(obj: Any, name: str, output_dir: Path) -> Path: + """ + Save a single object to file with appropriate format. + Internal helper function for save_results. + + Parameters + ---------- + obj : Any + Object to save + name : str + Base name for the file (extension will be added if needed) + output_dir : Path + Directory to save to + + Returns + ------- + Path + Path to saved file + """ + # Determine file format based on object type + if isinstance(obj, pd.DataFrame): + # DataFrames -> CSV + if not name.endswith('.csv'): + name = f"{name}.csv" + filepath = output_dir / name + obj.to_csv(filepath, index=False) + + elif hasattr(obj, 'savefig'): + # Matplotlib figures -> PNG only + if not name.endswith('.png'): + name = f"{name}.png" + filepath = output_dir / name + obj.savefig(filepath, dpi=300, bbox_inches='tight') + plt.close(obj) # Close figure to free memory + + elif isinstance(obj, str) and (' pickle (for consistency, could be h5ad) + if not name.endswith('.pickle'): + name = f"{name}.pickle" + filepath = output_dir / name + with open(filepath, 'wb') as f: + pickle.dump(obj, f) + + else: + # Everything else -> pickle + if '.' not in name: + name = f"{name}.pickle" + filepath = output_dir / name + with open(filepath, 'wb') as f: + pickle.dump(obj, f) + + logger.debug(f"Saved {type(obj).__name__} to {filepath}") + return filepath def parse_params( @@ -446,21 +623,48 @@ def spell_out_special_characters(text: str) -> str: return text +def clean_column_name(column_name: str) -> str: + """ + Clean a single column name using spell_out_special_characters. + + Parameters + ---------- + column_name : str + Original column name + + Returns + ------- + str + Cleaned column name + """ + original = column_name + cleaned = spell_out_special_characters(column_name) + # Ensure doesn't start with digit + if cleaned and cleaned[0].isdigit(): + cleaned = f'col_{cleaned}' + if original != cleaned: + logger.info(f'Column Name Updated: "{original}" -> "{cleaned}"') + return cleaned + + def load_csv_files( - csv_dir: Union[str, Path], + csv_input: Union[str, Path, List[str]], files_config: pd.DataFrame, string_columns: Optional[List[str]] = None ) -> pd.DataFrame: """ Load and combine CSV files based on configuration. + + Supports both: + - Galaxy input: list of file paths + - NIDAP input: directory path Parameters ---------- - csv_dir : str or Path - Directory containing CSV files + csv_input : str, Path, or list + Either a directory path (NIDAP) or list of file paths (Galaxy) files_config : pd.DataFrame - Configuration dataframe with 'file_name' column and optional - metadata + Configuration dataframe with 'file_name' column and optional metadata string_columns : list, optional Columns to force as string type @@ -470,11 +674,19 @@ def load_csv_files( Combined dataframe with all CSV data """ import pprint - from spac.data_utils import combine_dfs - from spac.utils import check_list_in_list - csv_dir = Path(csv_dir) - filename = "file_name" + filename_col = "file_name" + + # Build file path mapping based on input type + if isinstance(csv_input, list): + # Galaxy: list of file paths + file_path_map = {Path(p).name: Path(p) for p in csv_input} + logger.info(f"Galaxy mode: {len(file_path_map)} files provided") + else: + # NIDAP: directory path + csv_dir = Path(csv_input) + file_path_map = {p.name: p for p in csv_dir.glob("*.csv")} + logger.info(f"NIDAP mode: {len(file_path_map)} CSV files in {csv_dir}") # Clean configuration files_config = files_config.applymap( @@ -483,8 +695,8 @@ def load_csv_files( # Get column names all_column_names = files_config.columns.tolist() - filtered_column_names = [ - col for col in all_column_names if col not in [filename] + metadata_columns = [ + col for col in all_column_names if col != filename_col ] # Validate string_columns @@ -504,33 +716,18 @@ def load_csv_files( # Extract data types dtypes = files_config.dtypes.to_dict() - # Clean column names - def clean_column_name(column_name): - original = column_name - cleaned = spell_out_special_characters(column_name) - # Ensure doesn't start with digit - if cleaned and cleaned[0].isdigit(): - cleaned = f'col_{cleaned}' - if original != cleaned: - print(f'Column Name Updated: "{original}" -> "{cleaned}"') - return cleaned - # Get files to process files_config = files_config.astype(str) files_to_use = [ - f.strip() for f in files_config[filename].tolist() + f.strip() for f in files_config[filename_col].tolist() ] # Check all files exist - missing_files = [] - for file_name in files_to_use: - if not (csv_dir / file_name).exists(): - missing_files.append(file_name) - + missing_files = [f for f in files_to_use if f not in file_path_map] if missing_files: - raise TypeError( - f"The following files are not found: " - f"{', '.join(missing_files)}" + raise FileNotFoundError( + f"Files not found: {', '.join(missing_files)}\n" + f"Available: {', '.join(file_path_map.keys())}" ) # Prepare dtype override @@ -540,60 +737,35 @@ def clean_column_name(column_name): # Process files processed_df_list = [] - first_file = True for file_name in files_to_use: - file_path = csv_dir / file_name - file_locations = files_config[ - files_config[filename] == file_name - ].index.tolist() - - # Check for duplicate file names - if len(file_locations) > 1: - print( - f'Multiple entries for file: "{file_name}", exiting...' - ) - return None + file_path = file_path_map[file_name] try: current_df = pd.read_csv(file_path, dtype=dtype_override) - print(f'\nProcessing file: "{file_name}"') + logger.info(f'Processing: "{file_name}"') current_df.columns = [ clean_column_name(col) for col in current_df.columns ] - # Validate string_columns exist - if first_file and string_columns: - check_list_in_list( - input=string_columns, - input_name='string_columns', - input_type='column', - target_list=list(current_df.columns), - need_exist=True, - warning=False - ) - first_file = False - except pd.errors.EmptyDataError: - raise TypeError(f'The file: "{file_name}" is empty.') + raise ValueError(f'File "{file_name}" is empty.') except pd.errors.ParserError: - raise TypeError( - f'The file "{file_name}" could not be parsed. ' - 'Please check that the file is a valid CSV.' + raise ValueError( + f'File "{file_name}" could not be parsed as CSV.' ) - current_df[filename] = file_name + current_df[filename_col] = file_name - # Reorder columns - cols = current_df.columns.tolist() - cols.insert(0, cols.pop(cols.index(filename))) + # Reorder columns: filename first + cols = [filename_col] + [c for c in current_df.columns if c != filename_col] current_df = current_df[cols] processed_df_list.append(current_df) - print(f'File: "{file_name}" Processed!\n') + logger.info(f'File "{file_name}" processed: {current_df.shape}') # Combine dataframes - final_df = combine_dfs(processed_df_list) + final_df = pd.concat(processed_df_list, ignore_index=True) # Ensure string columns remain strings for col in string_columns: @@ -601,22 +773,18 @@ def clean_column_name(column_name): final_df[col] = final_df[col].astype(str) # Add metadata columns - if filtered_column_names: - for column in filtered_column_names: - # Map values from config + if metadata_columns: + for column in metadata_columns: file_to_value = ( - files_config.set_index(filename)[column].to_dict() + files_config.set_index(filename_col)[column].to_dict() ) - final_df[column] = final_df[filename].map(file_to_value) - # Ensure correct dtype + final_df[column] = final_df[filename_col].map(file_to_value) final_df[column] = final_df[column].astype(dtypes[column]) - print(f'\n\nColumn "{column}" Mapping: ') - pp = pprint.PrettyPrinter(indent=4) - pp.pprint(file_to_value) + logger.info(f'Added metadata column "{column}"') + logger.debug(f'Mapping: {file_to_value}') - print("\n\nFinal Dataframe Info") - print(final_df.info()) + logger.info(f"Combined {len(processed_df_list)} files -> {final_df.shape}") return final_df @@ -705,4 +873,4 @@ def string_list_to_dictionary( "\n".join(errors) ) - return parsed_dict \ No newline at end of file + return parsed_dict diff --git a/src/spac/templates/tsne_analysis_template.py b/src/spac/templates/tsne_analysis_template.py index b2366bd7..d72de6ec 100644 --- a/src/spac/templates/tsne_analysis_template.py +++ b/src/spac/templates/tsne_analysis_template.py @@ -2,6 +2,9 @@ Platform-agnostic tSNE Analysis template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.tsne_analysis_template import run_from_json @@ -18,36 +21,78 @@ from spac.transformations import tsne from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, ) def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], Any]: """ - Execute tSNE Analysis analysis with parameters from JSON. + Execute tSNE Analysis with parameters from JSON. Replicates the NIDAP template functionality exactly. Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the adata object + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Table_to_Process": "Original", + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If False, returns the AnnData object directly for in-memory workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. Returns ------- dict or AnnData - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed AnnData object + If save_to_disk=True: Dictionary of saved file paths with structure: + {"analysis": "path/to/output.pickle"} + If save_to_disk=False: The processed AnnData object + + Notes + ----- + Output Structure: + - Analysis output is saved as a single pickle file (standardized for analysis outputs) + - When save_to_disk=False, the AnnData object is returned for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["analysis"]) # Path to saved pickle file + + >>> # Get results in memory + >>> adata = run_from_json("params.json", save_to_disk=False) + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") """ # Parse parameters from JSON params = parse_params(json_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + # Analysis uses file type per standardized schema + if "outputs" not in params: + params["outputs"] = { + "analysis": {"type": "file", "name": "output.pickle"} + } + # Load the upstream analysis data all_data = load_input(params["Upstream_Analysis"]) @@ -66,22 +111,26 @@ def run_from_json( tsne(all_data, layer=Layer_to_Analysis) print("tSNE Done!") - + print(all_data) object_to_output = all_data - - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "transform_output.pickle") - # Default to pickle format if no recognized extension - if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): - output_file = output_file + '.pickle' - - saved_files = save_outputs({output_file: object_to_output}) - - print(f"tSNE Analysis completed → {saved_files[output_file]}") + + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary + results_dict = {} + if "analysis" in params["outputs"]: + results_dict["analysis"] = object_to_output + + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + print(f"tSNE Analysis completed → {saved_files['analysis']}") return saved_files else: # Return the adata object directly for in-memory workflows @@ -91,18 +140,24 @@ def run_from_json( # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python tsne_analysis_template.py ", + "Usage: python tsne_analysis_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) if isinstance(result, dict): print("\nOutput files:") for filename, filepath in result.items(): print(f" {filename}: {filepath}") else: - print("\nReturned AnnData object") \ No newline at end of file + print("\nReturned AnnData object") diff --git a/src/spac/templates/umap_transformation_template.py b/src/spac/templates/umap_transformation_template.py index 732d83fb..388f3d11 100644 --- a/src/spac/templates/umap_transformation_template.py +++ b/src/spac/templates/umap_transformation_template.py @@ -2,6 +2,9 @@ Platform-agnostic UMAP transformation template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.umap_transformation_template import run_from_json @@ -20,7 +23,7 @@ from spac.transformations import run_umap from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, text_to_value, ) @@ -28,7 +31,8 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], Any]: """ Execute UMAP transformation analysis with parameters from JSON. @@ -37,20 +41,62 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the adata object + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Table_to_Process": "Original", + "Number_of_Neighbors": 75, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If False, returns the AnnData object directly for in-memory workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. Returns ------- dict or AnnData - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed AnnData object + If save_to_disk=True: Dictionary of saved file paths with structure: + {"analysis": "path/to/output.pickle"} + If save_to_disk=False: The processed AnnData object + + Notes + ----- + Output Structure: + - Analysis output is saved as a single pickle file (standardized for analysis outputs) + - When save_to_disk=False, the AnnData object is returned for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["analysis"]) # Path to saved pickle file + + >>> # Get results in memory + >>> adata = run_from_json("params.json", save_to_disk=False) + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") """ # Parse parameters from JSON params = parse_params(json_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + # Analysis uses file type per standardized schema + if "outputs" not in params: + params["outputs"] = { + "analysis": {"type": "file", "name": "output.pickle"} + } + # Load the upstream analysis data adata = load_input(params["Upstream_Analysis"]) @@ -81,19 +127,21 @@ def run_from_json( # Print adata info as in NIDAP print(adata) - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "transform_output.pickle") - # Default to pickle format if no recognized extension - if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): - output_file = output_file + '.pickle' - - # Note: Following NIDAP pattern, save the transformed object - # (which is the same as adata if run_umap modifies in-place) - saved_files = save_outputs({output_file: updated_dataset}) - - print(f"UMAP transformation completed → {saved_files[output_file]}") + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary + results_dict = {} + if "analysis" in params["outputs"]: + results_dict["analysis"] = updated_dataset + + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + print(f"UMAP transformation completed → {saved_files['analysis']}") return saved_files else: # Return the adata object directly for in-memory workflows @@ -103,18 +151,24 @@ def run_from_json( # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - f"Usage: python umap_transformation_template.py ", + "Usage: python umap_transformation_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) if isinstance(result, dict): print("\nOutput files:") for filename, filepath in result.items(): print(f" {filename}: {filepath}") else: - print("\nReturned data object") \ No newline at end of file + print("\nReturned data object") diff --git a/src/spac/templates/umap_tsne_pca_visualization_template.py b/src/spac/templates/umap_tsne_pca_visualization_template.py new file mode 100644 index 00000000..47394b04 --- /dev/null +++ b/src/spac/templates/umap_tsne_pca_visualization_template.py @@ -0,0 +1,242 @@ +""" +Platform-agnostic UMAP\\tSNE\\PCA Visualization template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + +Usage +----- +>>> from spac.templates.umap_tsne_pca_template import run_from_json +>>> run_from_json("examples/umap_tsne_pca_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, Optional, List +import matplotlib.pyplot as plt +import logging + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.visualization import dimensionality_reduction_plot +from spac.templates.template_utils import ( + load_input, + save_results, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_to_disk: bool = True, + show_plot: bool = True, + output_dir: str = None, +) -> Union[Dict[str, Union[str, List[str]]], plt.Figure]: + """ + Execute UMAP\\tSNE\\PCA Visualization analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Color_By": "Annotation", + "Annotation_to_Highlight": "cell_type", + "Dimension_Reduction_Method": "umap", + ... + "outputs": { + "figures": {"type": "directory", "name": "figures_dir"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If False, returns the figure + directly for in-memory workflows. Default is True. + show_plot : bool, optional + Whether to display the plot. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. + + Returns + ------- + dict or Figure + If save_to_disk=True: Dictionary of saved file paths + If save_to_disk=False: The matplotlib figure + """ + # Set up logging + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + + # Parse parameters from JSON + params = parse_params(json_path) + + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + if "outputs" not in params: + params["outputs"] = { + "figures": {"type": "directory", "name": "figures_dir"} + } + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + annotation = params.get("Annotation_to_Highlight", "None") + feature = params.get("Feature_to_Highlight", "None") + layer = params.get("Table", "Original") + method = params.get("Dimension_Reduction_Method", "umap") + fig_width = params.get("Figure_Width", 12) + fig_height = params.get("Figure_Height", 12) + font_size = params.get("Font_Size", 12) + fig_dpi = params.get("Figure_DPI", 300) + legend_location = params.get("Legend_Location", "best") + legend_label_size = params.get("Legend_Font_Size", 16) + legend_marker_scale = params.get("Legend_Marker_Size", 5.0) + color_by = params.get("Color_By", "Annotation") + point_size = params.get("Dot_Size", 1) + v_min = params.get("Value_Min", "None") + v_max = params.get("Value_Max", "None") + + feature = text_to_value(feature) + annotation = text_to_value(annotation) + + if color_by == "Annotation": + feature = None + else: + annotation = None + + # Store the original value of layer + layer_input = layer + + layer = text_to_value(layer, default_none_text="Original") + + vmin = text_to_value( + v_min, + default_none_text="None", + value_to_convert_to=None, + to_float=True, + param_name="Value Min" + ) + + vmax = text_to_value( + v_max, + default_none_text="None", + value_to_convert_to=None, + to_float=True, + param_name="Value Max" + ) + + plt.rcParams.update({'font.size': font_size}) + + fig, ax = dimensionality_reduction_plot( + adata=adata, + method=method, + annotation=annotation, + feature=feature, + layer=layer, + point_size=point_size, + vmin=vmin, + vmax=vmax + ) + + if color_by == "Annotation": + title = annotation + else: + title = f'Table:"{layer_input}" \n Feature:"{feature}"' + ax.set_title(title) + + fig = ax.get_figure() + + fig.set_size_inches( + fig_width, + fig_height + ) + fig.set_dpi(fig_dpi) + + legend = ax.get_legend() + has_legend = legend is not None + + if has_legend: + ax.legend( + loc=legend_location, + bbox_to_anchor=(1, 0.5), + fontsize=legend_label_size, + markerscale=legend_marker_scale + ) + + plt.tight_layout() + + if show_plot: + plt.show() + + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + # Check for figures output + if "figures" in params["outputs"]: + results_dict["figures"] = {f"{method}_plot": fig} + + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + plt.close(fig) + + logger.info( + f"{method.upper()} Visualization completed successfully." + ) + return saved_files + else: + # Return the figure directly for in-memory workflows + logger.info("Returning figure for in-memory use") + return fig + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) < 2: + print( + "Usage: python umap_tsne_pca_template.py [output_dir]", + file=sys.stderr + ) + sys.exit(1) + + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + + if isinstance(result, dict): + print("\nOutput files:") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") + else: + print("\nReturned figure") diff --git a/src/spac/templates/utag_clustering_template.py b/src/spac/templates/utag_clustering_template.py index 2a20f7a2..304f6149 100644 --- a/src/spac/templates/utag_clustering_template.py +++ b/src/spac/templates/utag_clustering_template.py @@ -2,6 +2,9 @@ Platform-agnostic UTAG Clustering template converted from NIDAP. Maintains the exact logic from the NIDAP template. +Refactored to use centralized save_results from template_utils. +Reads outputs configuration from blueprint JSON file. + Usage ----- >>> from spac.templates.utag_clustering_template import run_from_json @@ -19,7 +22,7 @@ from spac.transformations import run_utag_clustering from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, parse_params, text_to_value, ) @@ -27,7 +30,8 @@ def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True + save_to_disk: bool = True, + output_dir: str = None, ) -> Union[Dict[str, str], Any]: """ Execute UTAG Clustering analysis with parameters from JSON. @@ -36,20 +40,62 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the adata object + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Table_to_Process": "Original", + "K_Nearest_Neighbors": 15, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If False, returns the AnnData object directly for in-memory workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. Returns ------- dict or AnnData - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed AnnData object + If save_to_disk=True: Dictionary of saved file paths with structure: + {"analysis": "path/to/output.pickle"} + If save_to_disk=False: The processed AnnData object + + Notes + ----- + Output Structure: + - Analysis output is saved as a single pickle file (standardized for analysis outputs) + - When save_to_disk=False, the AnnData object is returned for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["analysis"]) # Path to saved pickle file + + >>> # Get results in memory + >>> adata = run_from_json("params.json", save_to_disk=False) + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") """ # Parse parameters from JSON params = parse_params(json_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + # Analysis uses file type per standardized schema + if "outputs" not in params: + params["outputs"] = { + "analysis": {"type": "file", "name": "output.pickle"} + } + # Load the upstream analysis data adata = load_input(params["Upstream_Analysis"]) @@ -136,17 +182,21 @@ def run_from_json( print(label_counts) print("\n") - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "transform_output.pickle") - # Default to pickle format if no recognized extension - if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): - output_file = output_file + '.pickle' - - saved_files = save_outputs({output_file: adata}) - - print(f"UTAG Clustering completed → {saved_files[output_file]}") + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary + results_dict = {} + if "analysis" in params["outputs"]: + results_dict["analysis"] = adata + + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + print(f"UTAG Clustering completed → {saved_files['analysis']}") return saved_files else: # Return the adata object directly for in-memory workflows @@ -156,18 +206,24 @@ def run_from_json( # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( - "Usage: python utag_clustering_template.py ", + "Usage: python utag_clustering_template.py [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) if isinstance(result, dict): print("\nOutput files:") for filename, filepath in result.items(): print(f" {filename}: {filepath}") else: - print("\nReturned AnnData object") \ No newline at end of file + print("\nReturned AnnData object") diff --git a/src/spac/templates/visualize_nearest_neighbor_template.py b/src/spac/templates/visualize_nearest_neighbor_template.py index 34bc0338..42193531 100644 --- a/src/spac/templates/visualize_nearest_neighbor_template.py +++ b/src/spac/templates/visualize_nearest_neighbor_template.py @@ -9,10 +9,10 @@ ... ) >>> run_from_json("examples/visualize_nearest_neighbor_params.json") """ -import json +import logging import sys from pathlib import Path -from typing import Any, Dict, Union, Tuple, List +from typing import Any, Dict, List, Tuple, Union import pandas as pd import numpy as np from matplotlib.axes import Axes @@ -25,17 +25,21 @@ from spac.visualization import visualize_nearest_neighbor from spac.templates.template_utils import ( load_input, - save_outputs, parse_params, + save_results, text_to_value, ) +# Set up logging +logger = logging.getLogger(__name__) + def run_from_json( json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True, + save_to_disk: bool = True, + output_dir: Union[str, Path] = None, show_plot: bool = True -) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: +) -> Union[Dict[str, Union[str, List[str]]], Tuple[Any, pd.DataFrame]]: """ Execute Visualize Nearest Neighbor analysis with parameters from JSON. Replicates the NIDAP template functionality exactly. @@ -43,22 +47,71 @@ def run_from_json( Parameters ---------- json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the figure and + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/input.pickle", + "Annotation": "cell_type", + "Source_Anchor_Cell_Label": "CD4_T", + "Target_Cell_Label": "All", + "Plot_Method": "numeric", + "Plot_Type": "boxen", + "outputs": { + "figures": {"type": "directory", "name": "figures_dir"}, + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If False, returns the figure and dataframe directly for in-memory workflows. Default is True. + output_dir : str or Path, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. show_plot : bool, optional Whether to display the plot. Default is True. Returns ------- dict or tuple - If save_results=True: Dictionary of saved file paths - If save_results=False: Tuple of (figure(s), dataframe) + If save_to_disk=True: Dictionary of saved file paths with structure: + {"figures": ["path/to/fig1.png", ...], "dataframe": "path/to/df.csv"} + If save_to_disk=False: Tuple of (figure(s), dataframe) + + Notes + ----- + Output Structure: + - Figures are saved as a directory containing one or more plot files (standardized) + - DataFrame is saved as a single CSV file (standardized) + - When save_to_disk=False, returns (figure(s), dataframe) for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["figures"]) # List of figure paths + >>> print(saved_files["dataframe"]) # Path to CSV + + >>> # Get results in memory for further processing + >>> figures, df = run_from_json("params.json", save_to_disk=False) + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") """ # Parse parameters from JSON params = parse_params(json_path) + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + # Figures use directory type, dataframe uses file type per standardized schema + if "outputs" not in params: + params["outputs"] = { + "figures": {"type": "directory", "name": "figures"}, + "dataframe": {"type": "file", "name": "dataframe.csv"} + } + # Load the upstream analysis data adata = load_input(params["Upstream_Analysis"]) @@ -126,14 +179,13 @@ def run_from_json( # Configure Matplotlib font size plt.rcParams.update({'font.size': global_font_size}) - # If facet_plot=True but no valid stratify column => revert to - # single figure + # If facet_plot=True but no valid stratify column => revert to single figure if facet_plot and image_id is None: warning_message = ( "Facet plotting was requested, but there is no annotation " "to group by. Switching to a single-figure display." ) - print(warning_message) + logger.warning(warning_message) facet_plot = False result_dict = visualize_nearest_neighbor( @@ -157,8 +209,8 @@ def run_from_json( palette_hex = result_dict["palette"] axes_out = result_dict["ax"] - print("Summary statistics of the dataset:") - print(df_long.describe()) + logger.info("Summary statistics of the dataset:") + logger.info(f"\n{df_long.describe()}") # Customize figure legends & X-axis rotation legend_labels = ( @@ -256,21 +308,19 @@ def _flatten_axes(ax_input): n_unique = len(unique_vals) if n_unique == 0: - print( - f"[WARNING] The annotation '{image_id}' has 0 unique " - f"values or is empty. No data to plot => Potential " - f"empty plot." + logger.warning( + f"The annotation '{image_id}' has 0 unique values or is empty. " + "No data to plot => Potential empty plot." ) elif n_unique == 1 and facet_plot: - print( - f"[INFO] The annotation '{image_id}' has only one unique " - f"value ({unique_vals[0]}). Facet plot will resemble a " - f"single plot." + logger.info( + f"The annotation '{image_id}' has only one unique value " + f"({unique_vals[0]}). Facet plot will resemble a single plot." ) elif n_unique > 1: - print( - f"The annotation '{image_id}' has {n_unique} unique " - f"values: {unique_vals}" + logger.info( + f"The annotation '{image_id}' has {n_unique} unique values: " + f"{unique_vals}" ) # Figure Configuration & Display @@ -315,24 +365,22 @@ def _label_each_figure(fig_list, categories): else: cat_list = df_long[image_id].unique().tolist() - # Track figures for optional saving - figures = [] + # Track figures for saving + figures_to_save = [] if isinstance(figs_out, list) and not facet_plot and \ cat_list and len(figs_out) == len(cat_list): - # Scenario: Multiple separate figures, one per category - # (non-faceted) - figures = figs_out + # Scenario: Multiple separate figures, one per category (non-faceted) + figures_to_save = figs_out _label_each_figure(figs_out, cat_list) if show_plot: plt.show() else: - # Scenario: Single figure (faceted) or list of figures not - # matching categories + # Scenario: Single figure (faceted) or list of figures not matching categories figures_to_display = ( figs_out if isinstance(figs_out, list) else [figs_out] ) - figures = figures_to_display + figures_to_save = figures_to_display for fig_item_to_display in figures_to_display: if fig_item_to_display is not None: _title_main(fig_item_to_display, fig_title) @@ -351,7 +399,7 @@ def _label_each_figure(fig_list, categories): if show_plot: plt.show() - # summary statistics + # Summary statistics # 1) Per-group summary df_summary_group = ( df_long @@ -372,11 +420,11 @@ def _label_each_figure(fig_list, categories): df_summary_group_strat = None if df_summary_group_strat is not None: - print(f"\nSummary by group(target phenotypes) AND '{image_id}':") - print(df_summary_group_strat) + logger.info(f"\nSummary by group(target phenotypes) AND '{image_id}':") + logger.info(f"\n{df_summary_group_strat}") else: - print("\nSummary: By group(target phenotypes) only") - print(df_summary_group) + logger.info("\nSummary: By group(target phenotypes) only") + logger.info(f"\n{df_summary_group}") # CSV Output final_df = ( @@ -384,46 +432,92 @@ def _label_each_figure(fig_list, categories): else df_summary_group ) - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get( - "Output_File", "nearest_neighbor_plots.csv" - ) - saved_files = save_outputs({output_file: final_df}) - - print(f"\nSaved summary statistics to '{output_file}'.") - print( - f"Visualize Nearest Neighbor completed → " - f"{saved_files[output_file]}" + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + # Package figures in a dictionary for directory saving + # This ensures they're saved in a directory per standardized schema + if "figures" in params["outputs"] and figures_to_save: + # Create a dictionary with named figures + figures_dict = {} + for idx, fig in enumerate(figures_to_save): + if fig is not None: + # Name figures appropriately + if cat_list and len(cat_list) == len(figures_to_save): + fig_name = f"nearest_neighbor_{cat_list[idx]}" + else: + fig_name = f"nearest_neighbor_{idx}" + figures_dict[fig_name] = fig + results_dict["figures"] = figures_dict # Dict triggers directory save + + # Check for DataFrame output (case-insensitive) + if any(k.lower() == "dataframe" for k in params["outputs"].keys()): + results_dict["dataframe"] = final_df + + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir ) + + logger.info("Visualize Nearest Neighbor completed successfully.") + logger.info(f"Saved summary statistics to dataframe output.") return saved_files else: - # Return the figure(s) and dataframe directly for in-memory - # workflows - print("Returning figure(s) and dataframe (not saving to file)") + # Return the figure(s) and dataframe directly for in-memory workflows + logger.info("Returning figure(s) and dataframe (not saving to file)") # If single figure, return it directly; if multiple, return list - if len(figures) == 1: - return figures[0], final_df + if len(figures_to_save) == 1: + return figures_to_save[0], final_df else: - return figures, final_df + return figures_to_save, final_df # CLI interface if __name__ == "__main__": - if len(sys.argv) != 2: + if len(sys.argv) < 2: print( "Usage: python visualize_nearest_neighbor_template.py " - "", + " [output_dir]", file=sys.stderr ) sys.exit(1) - result = run_from_json(sys.argv[1]) + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + # Display results based on return type if isinstance(result, dict): print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") + for key, paths in result.items(): + if isinstance(paths, list): + print(f" {key}:") + for path in paths: + print(f" - {path}") + else: + print(f" {key}: {paths}") else: - print("\nReturned figure(s) and dataframe") + figures, df = result + print("\nReturned figure(s) and dataframe for in-memory use") + if isinstance(figures, list): + print(f"Number of figures: {len(figures)}") + else: + print(f"Figure size: {figures.get_size_inches()}") + print(f"DataFrame shape: {df.shape}") + print("\nSummary statistics preview:") + print(df.head()) diff --git a/src/spac/templates/visualize_ripley_l_template.py b/src/spac/templates/visualize_ripley_l_template.py new file mode 100644 index 00000000..17e8b7b2 --- /dev/null +++ b/src/spac/templates/visualize_ripley_l_template.py @@ -0,0 +1,155 @@ +""" +Platform-agnostic Visualize Ripley L template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Usage +----- +>>> from spac.templates.visualize_ripley_template import run_from_json +>>> run_from_json("examples/visualize_ripley_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union, List, Optional, Tuple +import pandas as pd +import matplotlib.pyplot as plt +import logging + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.visualization import plot_ripley_l +from spac.templates.template_utils import ( + load_input, + save_results, + parse_params, + text_to_value, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_to_disk: bool = True, + show_plot: bool = True, + output_dir: Optional[Union[str, Path]] = None +) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: + """ + Execute Visualize Ripley L analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary + save_to_disk : bool, optional + Whether to save results to file. If False, returns the figure and + dataframe directly for in-memory workflows. Default is True. + show_plot : bool, optional + Whether to display the plot. Default is True. + output_dir : str or Path, optional + Directory for outputs. If None, uses current directory. + + Returns + ------- + dict or tuple + If save_to_disk=True: Dictionary of saved file paths + If save_to_disk=False: Tuple of (figure, dataframe) + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + center_phenotype = params["Center_Phenotype"] + neighbor_phenotype = params["Neighbor_Phenotype"] + plot_specific_regions = params.get("Plot_Specific_Regions", False) + regions_labels = params.get("Regions_Labels", []) + plot_simulations = params.get("Plot_Simulations", True) + + logging.info(f"Running with center_phenotype: {center_phenotype}, neighbor_phenotype: {neighbor_phenotype}") + + # Process regions parameter exactly as in NIDAP template + if plot_specific_regions: + if len(regions_labels) == 0: + raise ValueError( + 'Please identify at least one region in the ' + '"Regions Label(s) parameter' + ) + else: + regions_labels = None + + # Run the visualization exactly as in NIDAP template + fig, plots_df = plot_ripley_l( + adata, + phenotypes=(center_phenotype, neighbor_phenotype), + regions=regions_labels, + sims=plot_simulations, + return_df=True + ) + + if show_plot: + plt.show() + + # Print the dataframe to console + logging.info(f"\n{plots_df.to_string()}") + + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + # Check for dataframe output in config + if "dataframe" in params["outputs"]: + results_dict["dataframe"] = plots_df + + # Add figure if configured (usually not in the original template) + # but we can add it as an enhancement + if "figures" in params.get("outputs", {}): + # Package figure in a dictionary for directory saving + results_dict["figures"] = {"ripley_l_plot": fig} + + # Add analysis output if in config (for compatibility) + if "analysis" in params.get("outputs", {}): + results_dict["analysis"] = adata + + # Use centralized save_results function + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + logging.info(f"Visualize Ripley L completed → {list(saved_files.keys())}") + return saved_files + else: + # Return the figure and dataframe directly for in-memory workflows + logging.info("Returning figure and dataframe (not saving to file)") + return fig, plots_df + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python visualize_ripley_template.py ", file=sys.stderr) + sys.exit(1) + + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json(sys.argv[1], output_dir=output_dir) + + if isinstance(result, dict): + print("\nOutput files:") + for filename, filepath in result.items(): + print(f" {filename}: {filepath}") + else: + print("\nReturned figure and dataframe") diff --git a/src/spac/templates/z_score_normalization_template.py b/src/spac/templates/z_score_normalization_template.py new file mode 100644 index 00000000..38e90a30 --- /dev/null +++ b/src/spac/templates/z_score_normalization_template.py @@ -0,0 +1,181 @@ +""" +Platform-agnostic Z-Score Normalization template converted from NIDAP. +Maintains the exact logic from the NIDAP template. + +Refactored to use centralized save_results from template_utils. +Follows standardized output schema where analysis is saved as a file. + +Usage +----- +>>> from spac.templates.zscore_normalization_template import run_from_json +>>> run_from_json("examples/zscore_normalization_params.json") +""" +import json +import sys +from pathlib import Path +from typing import Any, Dict, Union +import pandas as pd +import pickle +import logging + +# Add parent directory to path for imports +sys.path.append(str(Path(__file__).parent.parent.parent)) + +from spac.transformations import z_score_normalization +from spac.templates.template_utils import ( + load_input, + save_results, + parse_params, +) + + +def run_from_json( + json_path: Union[str, Path, Dict[str, Any]], + save_to_disk: bool = True, + output_dir: str = None, +) -> Union[Dict[str, str], Any]: + """ + Execute Z-Score Normalization analysis with parameters from JSON. + Replicates the NIDAP template functionality exactly. + + Parameters + ---------- + json_path : str, Path, or dict + Path to JSON file, JSON string, or parameter dictionary. + Expected JSON structure: + { + "Upstream_Analysis": "path/to/data.pickle", + "Table_to_Process": "Original", + "Output_Table_Name": "z_scores", + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"} + } + } + save_to_disk : bool, optional + Whether to save results to disk. If True, saves the AnnData object + to a pickle file. If False, returns the AnnData object directly + for in-memory workflows. Default is True. + output_dir : str, optional + Base directory for outputs. If None, uses params['Output_Directory'] + or current directory. All outputs will be saved relative to this directory. + + Returns + ------- + dict or AnnData + If save_to_disk=True: Dictionary of saved file paths with structure: + {"analysis": "path/to/output.pickle"} + If save_to_disk=False: The processed AnnData object for in-memory use + + Notes + ----- + Output Structure: + - Analysis output is saved as a single pickle file (standardized for analysis outputs) + - When save_to_disk=False, the AnnData object is returned for programmatic use + + Examples + -------- + >>> # Save results to disk + >>> saved_files = run_from_json("params.json") + >>> print(saved_files["analysis"]) # Path to saved pickle file + >>> # './output.pickle' + + >>> # Get results in memory for further processing + >>> adata = run_from_json("params.json", save_to_disk=False) + >>> # Can now work with adata object directly + + >>> # Custom output directory + >>> saved = run_from_json("params.json", output_dir="/custom/path") + """ + # Parse parameters from JSON + params = parse_params(json_path) + + # Set output directory + if output_dir is None: + output_dir = params.get("Output_Directory", ".") + + # Ensure outputs configuration exists with standardized defaults + # Analysis uses file type per standardized schema + if "outputs" not in params: + params["outputs"] = { + "analysis": {"type": "file", "name": "output.pickle"} + } + + # Load the upstream analysis data + adata = load_input(params["Upstream_Analysis"]) + + # Extract parameters + input_layer = params["Table_to_Process"] + output_layer = params["Output_Table_Name"] + + if input_layer == "Original": + input_layer = None + + z_score_normalization( + adata, + output_layer=output_layer, + input_layer=input_layer + ) + + # Convert the normalized layer to a DataFrame and print its summary + post_dataframe = adata.to_df(layer=output_layer) + logging.info(f"Z-score normalization summary:\n{post_dataframe.describe()}") + logging.info(f"Transformed data:\n{adata}") + + # Handle results based on save_to_disk flag + if save_to_disk: + # Prepare results dictionary based on outputs config + results_dict = {} + + if "analysis" in params["outputs"]: + results_dict["analysis"] = adata + + # Use centralized save_results function + # All file handling and logging is now done by save_results + saved_files = save_results( + results=results_dict, + params=params, + output_base_dir=output_dir + ) + + logging.info( + f"Z-Score Normalization completed → {saved_files['analysis']}" + ) + return saved_files + else: + # Return the adata object directly for in-memory workflows + logging.info("Returning AnnData object (not saving to file)") + return adata + + +# CLI interface +if __name__ == "__main__": + if len(sys.argv) < 2: + print( + "Usage: python zscore_normalization_template.py [output_dir]", + file=sys.stderr + ) + sys.exit(1) + + # Set up logging for CLI usage + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Get output directory if provided + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + # Run analysis + result = run_from_json( + json_path=sys.argv[1], + output_dir=output_dir + ) + + # Display results based on return type + if isinstance(result, dict): + print("\nOutput files:") + for key, path in result.items(): + print(f" {key}: {path}") + else: + print("\nReturned AnnData object for in-memory use") + print(f"AnnData: {result}") diff --git a/tests/templates/test_add_pin_color_rule.py b/tests/templates/test_add_pin_color_rule.py index 2e380deb..762da8d6 100644 --- a/tests/templates/test_add_pin_color_rule.py +++ b/tests/templates/test_add_pin_color_rule.py @@ -1,5 +1,10 @@ -# tests/templates/test_add_pin_color_rule_template.py -"""Unit tests for the Append Pin Color Rule template.""" +# tests/templates/test_add_pin_color_rule.py +""" +Real (non-mocked) unit test for the Append Pin Color Rule template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,143 +12,87 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" ) -from spac.templates.add_pin_color_rule_template import run_from_json +from spac.templates.append_pin_color_rule_template import run_from_json -def mock_adata(n_cells: int = 10) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) - obs = pd.DataFrame({ - "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], - "batch": (["Batch1", "Batch2"] * ((n_cells + 1) // 2))[:n_cells] - }) - x_mat = rng.normal(size=(n_cells, 3)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = ["Gene1", "Gene2", "Gene3"] - # Initialize uns dict for color rules - adata.uns = {} - return adata +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 4 cells for color rule assignment.""" + rng = np.random.default_rng(42) + X = rng.random((4, 2)) + obs = pd.DataFrame({"cell_type": ["A", "B", "A", "B"]}) + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + return ad.AnnData(X=X, obs=obs, var=var) class TestAddPinColorRuleTemplate(unittest.TestCase): - """Unit tests for the Append Pin Color Rule template.""" + """Real (non-mocked) tests for the append pin color rule template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "color_mapped_analysis" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters from NIDAP template - self.params = { + params = { "Upstream_Analysis": self.in_file, - "Label_Color_Map": ["TypeA:red", "TypeB:blue"], + "Label_Color_Map": ["A:red", "B:blue"], "Color_Map_Name": "_spac_colors", "Overwrite_Previous_Color_Map": True, - "Output_File": self.out_file, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - # Verify file was saved - self.assertTrue(len(result) > 0) - pickle_files = [f for f in result.values() if '.pickle' in str(f)] - self.assertTrue(len(pickle_files) > 0) - - # Test 2: Run without saving - result_no_save = run_from_json(self.params, save_results=False) - # Check appropriate return type based on template - self.assertIsInstance(result_no_save, ad.AnnData) - # Verify color map was added - self.assertIn("_spac_colors", result_no_save.uns) - self.assertIn("_spac_colors_summary", result_no_save.uns) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test invalid color format - params_bad = self.params.copy() - params_bad["Label_Color_Map"] = ["TypeA-red"] # Missing colon - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - # Check error message contains expected text - error_msg = str(context.exception) - self.assertIn("Missing ':' separator", error_msg) - - @patch('spac.templates.add_pin_color_rule_template.add_pin_color_rules') - def test_function_calls(self, mock_add_rules) -> None: - """Test that main function is called with correct parameters.""" - # Mock the main function to simulate adding summary to adata.uns - def side_effect_add_rules(adata, **kwargs): - color_map_name = kwargs.get('color_map_name', '_spac_colors') - adata.uns[f'{color_map_name}_summary'] = "Mock summary" - return None - - mock_add_rules.side_effect = side_effect_add_rules - - # Test with different parameters - params_alt = self.params.copy() - params_alt["Color_Map_Name"] = "custom_colors" - params_alt["Overwrite_Previous_Color_Map"] = False - params_alt["Label_Color_Map"] = [ - "CD4+ T cells:cyan", - "CD8+ T cells:royalblue", - "B cells:yellowgreen" - ] - - run_from_json(params_alt, save_results=False) - - # Verify function was called correctly - mock_add_rules.assert_called_once() - call_args = mock_add_rules.call_args - - # Check specific parameter conversions - self.assertEqual(call_args[1]['color_map_name'], "custom_colors") - self.assertEqual(call_args[1]['overwrite'], False) - - # Verify color dict was parsed correctly - expected_dict = { - "CD4+ T cells": "cyan", - "CD8+ T cells": "royalblue", - "B cells": "yellowgreen" - } - self.assertEqual(call_args[1]['label_color_dict'], expected_dict) + def test_add_pin_color_rule_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run pin color rule and verify outputs. + + Validates: + 1. saved_files dict has 'analysis' key + 2. Pickle exists and contains AnnData + 3. Color map is stored in .uns + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, + ) + + self.assertIsInstance(saved_files, dict) + self.assertIn("analysis", saved_files) + + pkl_path = Path(saved_files["analysis"]) + self.assertTrue(pkl_path.exists()) + self.assertGreater(pkl_path.stat().st_size, 0) + + with open(pkl_path, "rb") as f: + result_adata = pickle.load(f) + self.assertIsInstance(result_adata, ad.AnnData) + self.assertIn("_spac_colors", result_adata.uns) + + mem_adata = run_from_json(self.json_file, save_to_disk=False) + self.assertIsInstance(mem_adata, ad.AnnData) + self.assertIn("_spac_colors", mem_adata.uns) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_analysis_to_csv_template.py b/tests/templates/test_analysis_to_csv_template.py index 7a9bdb53..3eaeb051 100644 --- a/tests/templates/test_analysis_to_csv_template.py +++ b/tests/templates/test_analysis_to_csv_template.py @@ -1,5 +1,10 @@ # tests/templates/test_analysis_to_csv_template.py -"""Unit tests for the Analysis to CSV template.""" +""" +Real (non-mocked) unit test for the Analysis to CSV template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,13 +12,11 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -22,116 +25,74 @@ from spac.templates.analysis_to_csv_template import run_from_json -def mock_adata(n_cells: int = 10) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) - obs = pd.DataFrame({ - "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], - "batch": (["Batch1", "Batch2"] * ((n_cells + 1) // 2))[:n_cells] - }) - x_mat = rng.normal(size=(n_cells, 3)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = ["Marker1", "Marker2", "Marker3"] - # Add spatial coordinates - adata.obsm["spatial"] = rng.random((n_cells, 2)) * 100 - # Add a test layer - adata.layers["normalized"] = rng.normal(size=(n_cells, 3)) +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 4 cells, 2 genes for CSV export.""" + rng = np.random.default_rng(42) + X = rng.random((4, 2)) + obs = pd.DataFrame({"cell_type": ["A", "B", "A", "B"]}) + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + adata = ad.AnnData(X=X, obs=obs, var=var) + adata.obsm["spatial"] = rng.random((4, 2)) * 100 return adata class TestAnalysisToCSVTemplate(unittest.TestCase): - """Unit tests for the Analysis to CSV template.""" + """Real (non-mocked) tests for the analysis to CSV template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "analysis.csv" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters - from NIDAP template - self.params = { + params = { "Upstream_Analysis": self.in_file, "Table_to_Export": "Original", - "Save_as_CSV_File": False, - "Output_File": self.out_file, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with Save_as_CSV_File=False (returns DataFrame) - result = run_from_json(self.params) - self.assertIsInstance(result, pd.DataFrame) - # Verify dataframe has expected columns - self.assertIn("Marker1", result.columns) - self.assertIn("Marker2", result.columns) - self.assertIn("Marker3", result.columns) - self.assertIn("cell_type", result.columns) - self.assertIn("spatial_x", result.columns) - self.assertIn("spatial_y", result.columns) - - # Test 2: Run with Save_as_CSV_File=True - params_save = self.params.copy() - params_save["Save_as_CSV_File"] = True - result_save = run_from_json(params_save) - self.assertIsInstance(result_save, dict) - self.assertIn(self.out_file, result_save) - - # Test 3: Export specific layer - params_layer = self.params.copy() - params_layer["Table_to_Export"] = "normalized" - result_layer = run_from_json(params_layer) - self.assertIsInstance(result_layer, pd.DataFrame) - # Verify it has the normalized data - self.assertEqual(len(result_layer), 10) # n_cells - - # Test 4: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, pd.DataFrame) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test missing layer - params_bad = self.params.copy() - params_bad["Table_to_Export"] = "nonexistent_layer" - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - # Check that error mentions the missing layer - self.assertIn("nonexistent_layer", str(context.exception)) - - @patch('spac.templates.analysis_to_csv_template.check_table') - def test_function_calls(self, mock_check) -> None: - """Test that main function is called with correct parameters.""" - # Run with a specific layer - params_with_layer = self.params.copy() - params_with_layer["Table_to_Export"] = "normalized" - - result = run_from_json(params_with_layer) - - # Verify check_table was called for the layer - mock_check.assert_called_once() - call_args = mock_check.call_args - self.assertEqual(call_args[1]['tables'], "normalized") - - # Verify result is a DataFrame - self.assertIsInstance(result, pd.DataFrame) + def test_analysis_to_csv_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: export AnnData to CSV and verify outputs. + + Validates: + 1. saved_files dict has 'dataframe' key + 2. CSV exists, is non-empty + 3. CSV has expected columns (genes + obs) + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, + ) + + self.assertIsInstance(saved_files, dict) + self.assertIn("dataframe", saved_files) + + csv_path = Path(saved_files["dataframe"]) + self.assertTrue(csv_path.exists()) + self.assertGreater(csv_path.stat().st_size, 0) + + result_df = pd.read_csv(csv_path) + # Should have gene columns and obs columns + self.assertIn("Gene_0", result_df.columns) + self.assertIn("Gene_1", result_df.columns) + self.assertEqual(len(result_df), 4) + + mem_df = run_from_json(self.json_file, save_to_disk=False) + self.assertIsInstance(mem_df, pd.DataFrame) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_append_annotation_template.py b/tests/templates/test_append_annotation_template.py index 2cdfe810..fcec1b56 100644 --- a/tests/templates/test_append_annotation_template.py +++ b/tests/templates/test_append_annotation_template.py @@ -1,17 +1,23 @@ # tests/templates/test_append_annotation_template.py -"""Unit tests for the Append Annotation template.""" +""" +Real (non-mocked) unit test for the Append Annotation template. + +Validates template I/O behaviour only: + - Expected output files are produced on disk + - Filenames follow the convention + - Output artifacts are non-empty + +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os -import pickle import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -20,204 +26,89 @@ from spac.templates.append_annotation_template import run_from_json -def mock_dataframe(n_rows: int = 10) -> pd.DataFrame: - """Return a minimal synthetic DataFrame for fast tests.""" - data = { - "cell_id": list(range(n_rows)), - "intensity": [i * 0.5 for i in range(n_rows)], - "existing_annotation": (["TypeA", "TypeB"] * - ((n_rows + 1) // 2))[:n_rows] - } - return pd.DataFrame(data) +def _make_tiny_dataframe() -> pd.DataFrame: + """ + Minimal synthetic DataFrame for append annotation testing. + + 4 rows, 2 columns -- the smallest dataset that exercises the + template's column-append code path. + """ + return pd.DataFrame({ + "cell_type": ["B cell", "T cell", "B cell", "T cell"], + "marker": [1.0, 2.0, 3.0, 4.0], + }) class TestAppendAnnotationTemplate(unittest.TestCase): - """Unit tests for the Append Annotation template.""" + """Real (non-mocked) tests for the append annotation template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.csv" - ) - self.out_file = "append_observations.csv" + self.in_file = os.path.join(self.tmp_dir.name, "input.csv") - # Save minimal mock data as CSV - mock_df = mock_dataframe() - mock_df.to_csv(self.in_file, index=False) + _make_tiny_dataframe().to_csv(self.in_file, index=False) - # Minimal parameters from NIDAP template - self.params = { + params = { "Upstream_Dataset": self.in_file, - "Annotation_Pair_List": ["region:region-A", "day:day1"], - "Output_File": self.out_file, + "Annotation_Pair_List": ["batch_id:batch_1", "site:lung"], + "Output_Directory": self.tmp_dir.name, + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with CSV input and save results - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - self.assertIn(self.out_file, result) - - # Verify the output file exists - output_path = result[self.out_file] - self.assertTrue(os.path.exists(output_path)) - - # Load and verify content - output_df = pd.read_csv(output_path) - self.assertIn("region", output_df.columns) - self.assertIn("day", output_df.columns) - self.assertTrue(all(output_df["region"] == "region-A")) - self.assertTrue(all(output_df["day"] == "day1")) - - # Test 2: Run without saving - result_no_save = run_from_json(self.params, save_results=False) - # Check appropriate return type - self.assertIsInstance(result_no_save, pd.DataFrame) - self.assertIn("region", result_no_save.columns) - self.assertIn("day", result_no_save.columns) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test invalid annotation pair format (missing colon) - params_bad = self.params.copy() - params_bad["Annotation_Pair_List"] = ["invalidformat"] - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad, save_results=False) - - # The error will come from the split operation - self.assertIn("not enough values to unpack", str(context.exception)) - - @patch('spac.templates.append_annotation_template.append_annotation') - @patch('spac.templates.append_annotation_template.check_column_name') - def test_function_calls(self, mock_check, mock_append) -> None: - """Test that main functions are called with correct parameters.""" - # Mock the append_annotation function to return a dataframe - # with the expected new columns - output_df = mock_dataframe() - output_df["region"] = "region-A" - output_df["day"] = "day1" - mock_append.return_value = output_df - - result = run_from_json(self.params, save_results=False) - - # Verify check_column_name was called for each pair - self.assertEqual(mock_check.call_count, 2) - mock_check.assert_any_call("region", "region:region-A") - mock_check.assert_any_call("day", "day:day1") - - # Verify append_annotation was called correctly - mock_append.assert_called_once() - call_args = mock_append.call_args - expected_dict = {"region": "region-A", "day": "day1"} - self.assertEqual(call_args[0][1], expected_dict) - - # Verify result is correct - self.assertIsInstance(result, pd.DataFrame) - self.assertIn("region", result.columns) - self.assertIn("day", result.columns) - - def test_pickle_input(self) -> None: - """Test that pickle input files work correctly.""" - # Create pickle input file - pickle_file = os.path.join(self.tmp_dir.name, "input.pkl") - mock_df = mock_dataframe() - with open(pickle_file, 'wb') as f: - pickle.dump(mock_df, f) - - params_pickle = self.params.copy() - params_pickle["Upstream_Dataset"] = pickle_file - - result = run_from_json(params_pickle, save_results=False) - self.assertIsInstance(result, pd.DataFrame) - self.assertIn("region", result.columns) - self.assertIn("day", result.columns) - self.assertEqual(len(result), 10) - - def test_dataframe_input(self) -> None: - """Test that direct DataFrame input works correctly.""" - # Pass DataFrame directly - params_df = self.params.copy() - params_df["Upstream_Dataset"] = mock_dataframe() - - result = run_from_json(params_df, save_results=False) - self.assertIsInstance(result, pd.DataFrame) - self.assertIn("region", result.columns) - self.assertIn("day", result.columns) - self.assertEqual(len(result), 10) - - def test_unsupported_file_format(self) -> None: - """Test error for unsupported file formats.""" - # Create a file with unsupported extension - bad_file = os.path.join(self.tmp_dir.name, "input.txt") - with open(bad_file, 'w') as f: - f.write("dummy content") - - params_bad = self.params.copy() - params_bad["Upstream_Dataset"] = bad_file - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - self.assertIn("Unsupported file format: .txt", str(context.exception)) - self.assertIn("Supported formats: .csv, .pickle, .pkl", - str(context.exception)) - - def test_invalid_input_type(self) -> None: - """Test error for invalid input types.""" - params_bad = self.params.copy() - params_bad["Upstream_Dataset"] = 12345 # Invalid type - - with self.assertRaises(TypeError) as context: - run_from_json(params_bad) - - self.assertIn("Upstream_Dataset must be DataFrame or file path", - str(context.exception)) - self.assertIn("Got ", str(context.exception)) - - @patch('builtins.print') - def test_console_output(self, mock_print) -> None: - """Test that expected console output is produced.""" - run_from_json(self.params, save_results=False) - - # Collect all printed output as strings - print_output = [] - for call in mock_print.call_args_list: - if call[0]: # If there are positional arguments - print_output.append(str(call[0][0])) - - # Join all output for easier searching - all_output = '\n'.join(print_output) - - # Check for key expected outputs - # Should print the annotation pairs dictionary - self.assertIn("region", all_output) - self.assertIn("region-A", all_output) - self.assertIn("day", all_output) - self.assertIn("day1", all_output) - - # Should show DataFrame info or similar output - self.assertTrue( - "DataFrame" in all_output or - "Returning DataFrame" in all_output or - "columns" in all_output + def test_append_annotation_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run append annotation template and verify + output artifacts. + + Validates: + 1. saved_files is a dict with 'dataframe' key + 2. Output CSV exists and is non-empty + 3. New annotation columns are present in the output + 4. In-memory return is a DataFrame with the appended columns + """ + # -- Act (save_to_disk=True) ----------------------------------- + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) + # -- Assert: return type --------------------------------------- + self.assertIsInstance(saved_files, dict) + + # -- Assert: CSV file exists and is non-empty ------------------ + self.assertIn("dataframe", saved_files) + csv_path = Path(saved_files["dataframe"]) + self.assertTrue(csv_path.exists(), f"CSV not found: {csv_path}") + self.assertGreater(csv_path.stat().st_size, 0) + + # -- Assert: appended columns present -------------------------- + result_df = pd.read_csv(csv_path) + self.assertIn("batch_id", result_df.columns) + self.assertIn("site", result_df.columns) + self.assertEqual(result_df["batch_id"].unique().tolist(), ["batch_1"]) + self.assertEqual(result_df["site"].unique().tolist(), ["lung"]) + + # -- Act (save_to_disk=False) ---------------------------------- + mem_df = run_from_json( + self.json_file, + save_to_disk=False, + ) + + # -- Assert: in-memory return is DataFrame --------------------- + self.assertIsInstance(mem_df, pd.DataFrame) + self.assertIn("batch_id", mem_df.columns) + self.assertIn("site", mem_df.columns) + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_arcsinh_normalization_template.py b/tests/templates/test_arcsinh_normalization_template.py index 46b7dda2..9e9c61f3 100644 --- a/tests/templates/test_arcsinh_normalization_template.py +++ b/tests/templates/test_arcsinh_normalization_template.py @@ -1,5 +1,10 @@ # tests/templates/test_arcsinh_normalization_template.py -"""Unit tests for the Arcsinh Normalization template.""" +""" +Real (non-mocked) unit test for the Arcsinh Normalization template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,12 +12,11 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path + import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -21,138 +25,74 @@ from spac.templates.arcsinh_normalization_template import run_from_json -def mock_adata(n_cells: int = 10) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) - # Create expression data with some high values to normalize - x_mat = rng.exponential(scale=10, size=(n_cells, 5)) - obs = pd.DataFrame({ - "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], - "batch": (["Batch1", "Batch2"] * ((n_cells + 1) // 2))[:n_cells] - }) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = [f"Marker{i}" for i in range(5)] - # Add an empty layers dict - adata.layers = {} - return adata +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 4 cells, 2 genes with positive values.""" + rng = np.random.default_rng(42) + X = rng.integers(1, 100, size=(4, 2)).astype(float) + obs = pd.DataFrame({"cell_type": ["A", "B", "A", "B"]}) + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + return ad.AnnData(X=X, obs=obs, var=var) class TestArcsinhNormalizationTemplate(unittest.TestCase): - """Unit tests for the Arcsinh Normalization template.""" + """Real (non-mocked) tests for the arcsinh normalization template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "output" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters from NIDAP template - self.params = { + params = { "Upstream_Analysis": self.in_file, - "Table_to_Process": "Original", - "Co_Factor": "5.0", + "Co_Factor": "5", "Percentile": "None", - "Output_Table_Name": "arcsinh", - "Per_Batch": "False", - "Annotation": "None", - "Output_File": self.out_file, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - @patch('spac.templates.arcsinh_normalization_template.arcsinh_transformation') - def test_complete_io_workflow(self, mock_arcsinh) -> None: - """Single I/O test covering input/output scenarios.""" - # Mock the arcsinh_transformation function - def mock_transform(adata, **kwargs): - # Simulate transformation by adding a layer - adata.layers[kwargs['output_layer']] = np.log1p(adata.X) / 5.0 - return adata - - mock_arcsinh.side_effect = mock_transform - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - # Verify file was saved - self.assertTrue(len(result) > 0) - pickle_files = [f for f in result.values() if '.pickle' in str(f)] - self.assertTrue(len(pickle_files) > 0) - - # Test 2: Run without saving - result_no_save = run_from_json(self.params, save_results=False) - # Check appropriate return type based on template - self.assertIsInstance(result_no_save, ad.AnnData) - self.assertIn("arcsinh", result_no_save.layers) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - # Verify arcsinh_transformation was called with correct parameters - call_args = mock_arcsinh.call_args - self.assertEqual(call_args[1]['input_layer'], None) # "Original" → None - self.assertEqual(call_args[1]['co_factor'], 5.0) - self.assertEqual(call_args[1]['percentile'], None) - self.assertEqual(call_args[1]['output_layer'], "arcsinh") - self.assertEqual(call_args[1]['per_batch'], False) - self.assertEqual(call_args[1]['annotation'], None) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test invalid float conversion for co_factor - params_bad = self.params.copy() - params_bad["Co_Factor"] = "invalid_number" - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - # Check exact error message - expected_msg = ( - "Error: can't convert co_factor to float. " - "Received:\"invalid_number\"" + def test_arcsinh_normalization_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run arcsinh normalization and verify outputs. + + Validates: + 1. saved_files is a dict with 'analysis' key + 2. Output pickle exists and is non-empty + 3. Output pickle contains an AnnData with 'arcsinh' layer + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) - self.assertEqual(str(context.exception), expected_msg) - - @patch('spac.templates.arcsinh_normalization_template.arcsinh_transformation') - def test_function_calls(self, mock_arcsinh) -> None: - """Test that main function is called with correct parameters.""" - # Mock the main function to return transformed data - mock_arcsinh.return_value = mock_adata() - mock_arcsinh.return_value.layers["arcsinh"] = np.zeros((10, 5)) - - # Test with percentile instead of co_factor - params_alt = self.params.copy() - params_alt["Co_Factor"] = "None" - params_alt["Percentile"] = "10" - params_alt["Per_Batch"] = "True" - params_alt["Annotation"] = "batch" - - run_from_json(params_alt, save_results=False) - - # Verify function was called correctly - mock_arcsinh.assert_called_once() - call_args = mock_arcsinh.call_args - - # Check specific parameter conversions - self.assertEqual(call_args[1]['co_factor'], None) - self.assertEqual(call_args[1]['percentile'], 10.0) - self.assertEqual(call_args[1]['per_batch'], True) - self.assertEqual(call_args[1]['annotation'], "batch") + + self.assertIsInstance(saved_files, dict) + self.assertIn("analysis", saved_files) + + pkl_path = Path(saved_files["analysis"]) + self.assertTrue(pkl_path.exists(), f"Pickle not found: {pkl_path}") + self.assertGreater(pkl_path.stat().st_size, 0) + + with open(pkl_path, "rb") as f: + result_adata = pickle.load(f) + self.assertIsInstance(result_adata, ad.AnnData) + self.assertIn("arcsinh", result_adata.layers) + + # -- save_to_disk=False returns AnnData in memory -------------- + mem_adata = run_from_json(self.json_file, save_to_disk=False) + self.assertIsInstance(mem_adata, ad.AnnData) + self.assertIn("arcsinh", mem_adata.layers) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_binary_to_categorical_annotation_template.py b/tests/templates/test_binary_to_categorical_annotation_template.py index eb5cb2f5..9e5a44f4 100644 --- a/tests/templates/test_binary_to_categorical_annotation_template.py +++ b/tests/templates/test_binary_to_categorical_annotation_template.py @@ -1,174 +1,117 @@ # tests/templates/test_binary_to_categorical_annotation_template.py -"""Unit tests for the Binary to Categorical Annotation template.""" +""" +Real (non-mocked) unit test for the Binary to Categorical Annotation template. + +Validates template I/O behaviour only: + - Expected output files are produced on disk + - Filenames follow the convention + - Output artifacts are non-empty + +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os -import pickle import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" ) from spac.templates.binary_to_categorical_annotation_template import ( - run_from_json + run_from_json, ) -def mock_dataframe(n_cells: int = 10) -> pd.DataFrame: - """Return a minimal synthetic DataFrame for fast tests.""" - # Create binary annotation columns with mutually exclusive values - data = { - "cell_id": [f"cell_{i}" for i in range(n_cells)], - "Normal_Cells": [0] * n_cells, - "Cancer_Cells": [0] * n_cells, - "Immuno_Cells": [0] * n_cells, - "x_centroid": [100.0 + i for i in range(n_cells)], - "y_centroid": [200.0 + i for i in range(n_cells)] - } - - # Make mutually exclusive binary annotations - for i in range(n_cells): - if i % 3 == 0: - data["Normal_Cells"][i] = 1 - elif i % 3 == 1: - data["Cancer_Cells"][i] = 1 - else: - data["Immuno_Cells"][i] = 1 - - return pd.DataFrame(data) +def _make_tiny_dataframe() -> pd.DataFrame: + """ + Minimal synthetic DataFrame with binary one-hot columns. + + 4 rows -- each row has exactly one 1 across the binary columns. + """ + return pd.DataFrame({ + "B_cell": [1, 0, 0, 0], + "T_cell": [0, 1, 0, 1], + "NK_cell": [0, 0, 1, 0], + "marker": [1.5, 2.5, 3.5, 4.5], + }) class TestBinaryToCategoricalAnnotationTemplate(unittest.TestCase): - """Unit tests for the Binary to Categorical Annotation template.""" + """Real (non-mocked) tests for the binary-to-categorical template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.csv" - ) - self.out_file = "converted_annotations.csv" + self.in_file = os.path.join(self.tmp_dir.name, "input.csv") - # Save minimal mock data - mock_df = mock_dataframe() - mock_df.to_csv(self.in_file, index=False) + _make_tiny_dataframe().to_csv(self.in_file, index=False) - # Minimal parameters from NIDAP template - self.params = { + params = { "Upstream_Dataset": self.in_file, - "Binary_Annotation_Columns": [ - "Normal_Cells", - "Cancer_Cells", - "Immuno_Cells" - ], + "Binary_Annotation_Columns": ["B_cell", "T_cell", "NK_cell"], "New_Annotation_Name": "cell_labels", - "Output_File": self.out_file, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters (CSV input) - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - self.assertIn(self.out_file, result) - - # Verify the output file exists and has expected content - output_path = result[self.out_file] - self.assertTrue(os.path.exists(output_path)) - - # Load and check the converted dataframe - converted_df = pd.read_csv(output_path) - self.assertIn("cell_labels", converted_df.columns) - # Check that categorical values were created - unique_labels = set(converted_df["cell_labels"]) - expected_labels = {"Normal_Cells", "Cancer_Cells", "Immuno_Cells"} - self.assertEqual(unique_labels, expected_labels) - - # Test 2: Run without saving - result_no_save = run_from_json(self.params, save_results=False) - self.assertIsInstance(result_no_save, pd.DataFrame) - self.assertIn("cell_labels", result_no_save.columns) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - # Test 4: Pickle file input - pickle_file = os.path.join(self.tmp_dir.name, "input.pkl") - mock_df = mock_dataframe() - with open(pickle_file, 'wb') as f: - pickle.dump(mock_df, f) - - params_pickle = self.params.copy() - params_pickle["Upstream_Dataset"] = pickle_file - result_pickle = run_from_json(params_pickle, save_results=False) - self.assertIsInstance(result_pickle, pd.DataFrame) - self.assertIn("cell_labels", result_pickle.columns) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test 1: Invalid column name with special characters - params_bad = self.params.copy() - params_bad["New_Annotation_Name"] = "cell@labels!" - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - # Check that check_column_name was triggered - self.assertIn("New Annotation Name", str(context.exception)) - - # Test 2: Unsupported file format - params_bad_format = self.params.copy() - params_bad_format["Upstream_Dataset"] = "input.txt" - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad_format) - - expected_msg = ( - "Unsupported file format: .txt. " - "Supported formats: .csv, .pickle, .pkl, .p" + def test_bin2cat_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run binary-to-categorical template and verify + output artifacts. + + Validates: + 1. saved_files is a dict with 'dataframe' key + 2. Output CSV exists and is non-empty + 3. New categorical column 'cell_labels' is present + 4. Categorical values match the original binary column names + """ + # -- Act (save_to_disk=True) ----------------------------------- + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) - self.assertEqual(str(context.exception), expected_msg) - - @patch('spac.templates.binary_to_categorical_annotation_template.bin2cat') - def test_function_calls(self, mock_bin2cat) -> None: - """Test that main function is called with correct parameters.""" - # Mock the bin2cat function to return a dataframe - mock_result_df = mock_dataframe() - mock_result_df["cell_labels"] = ["Normal_Cells"] * len(mock_result_df) - mock_bin2cat.return_value = mock_result_df - - run_from_json(self.params, save_results=False) - - # Verify function was called correctly - mock_bin2cat.assert_called_once() - call_args = mock_bin2cat.call_args - - # Check the arguments - self.assertIsInstance(call_args[1]['data'], pd.DataFrame) - self.assertEqual( - call_args[1]['one_hot_annotations'], - ["Normal_Cells", "Cancer_Cells", "Immuno_Cells"] + + # -- Assert: return type --------------------------------------- + self.assertIsInstance(saved_files, dict) + + # -- Assert: CSV file exists and is non-empty ------------------ + self.assertIn("dataframe", saved_files) + csv_path = Path(saved_files["dataframe"]) + self.assertTrue(csv_path.exists(), f"CSV not found: {csv_path}") + self.assertGreater(csv_path.stat().st_size, 0) + + # -- Assert: categorical column present with expected values --- + result_df = pd.read_csv(csv_path) + self.assertIn("cell_labels", result_df.columns) + expected_labels = {"B_cell", "T_cell", "NK_cell"} + actual_labels = set(result_df["cell_labels"].dropna().unique()) + self.assertEqual(actual_labels, expected_labels) + + # -- Act (save_to_disk=False) ---------------------------------- + mem_df = run_from_json( + self.json_file, + save_to_disk=False, ) - self.assertEqual(call_args[1]['new_annotation'], "cell_labels") + + # -- Assert: in-memory return is DataFrame --------------------- + self.assertIsInstance(mem_df, pd.DataFrame) + self.assertIn("cell_labels", mem_df.columns) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_boxplot_template.py b/tests/templates/test_boxplot_template.py index adccf3f5..3588f618 100644 --- a/tests/templates/test_boxplot_template.py +++ b/tests/templates/test_boxplot_template.py @@ -1,5 +1,14 @@ # tests/templates/test_boxplot_template.py -"""Unit tests for the Boxplot template.""" +""" +Real (non-mocked) unit test for the Boxplot template. + +Snowball seed test — validates template I/O behaviour only: + • Expected output files are produced on disk + • Filenames follow the convention + • Output artifacts are non-empty + +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,8 +16,7 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import matplotlib matplotlib.use("Agg") # Headless backend for CI @@ -16,7 +24,6 @@ import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -25,398 +32,162 @@ from spac.templates.boxplot_template import run_from_json -def mock_adata_for_boxplot(n_cells: int = 20) -> ad.AnnData: - """Return a minimal synthetic AnnData for boxplot tests.""" - rng = np.random.default_rng(0) - - # Create observations with annotations - obs = pd.DataFrame({ - "cell_type": ["B cells", "T cells"] * (n_cells // 2), - "condition": ["Control", "Treatment"] * (n_cells // 2) - }) - - # Create expression matrix with 3 features - n_features = 3 - x_mat = rng.poisson(lam=5, size=(n_cells, n_features)) + 1.0 - - # Create var dataframe with feature names - var = pd.DataFrame( - index=[f"Gene_{i}" for i in range(n_features)] +def _make_tiny_adata() -> ad.AnnData: + """ + Minimal synthetic AnnData for boxplot template testing. + + 4 cells, 2 genes, 2 cell types — the smallest dataset that exercises + the template's grouping, plotting, and summary-stats code paths. + """ + rng = np.random.default_rng(42) + + # 4 cells × 2 genes — small enough to reason about, + # large enough for describe() to return meaningful stats + n_cells, n_genes = 4, 2 + X = rng.integers(1, 10, size=(n_cells, n_genes)).astype(float) + + obs = pd.DataFrame( + {"cell_type": ["B cell", "T cell", "B cell", "T cell"]}, ) - - adata = ad.AnnData(X=x_mat, obs=obs, var=var) - - # Add a normalized layer - adata.layers["normalized"] = np.log1p(adata.X) - - return adata + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + return ad.AnnData(X=X, obs=obs, var=var) -class TestBoxplotTemplate(unittest.TestCase): - """Unit tests for the Boxplot template.""" - def _create_mock_boxplot_return(self, df_data=None): - """Helper to create standard mock returns.""" - mock_fig = MagicMock() - mock_ax = MagicMock() - if df_data is None: - df_data = {'Gene_0': [1]} - mock_df = pd.DataFrame(df_data) - return mock_fig, mock_ax, mock_df +class TestBoxplotTemplate(unittest.TestCase): + """Real (non-mocked) tests for the boxplot template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input_data.pickle" - ) - self.out_file = "boxplot_summary.csv" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data as pickle - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata_for_boxplot(), f) + # Save minimal real data as pickle (simulates upstream analysis) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters - self.params = { + # Write a JSON params file — the actual input the template receives + # in production (from Galaxy / Code Ocean) + params = { "Upstream_Analysis": self.in_file, "Primary_Annotation": "cell_type", "Secondary_Annotation": "None", "Table_to_Visualize": "Original", "Feature_s_to_Plot": ["All"], "Value_Axis_Log_Scale": False, - "Figure_Title": "BoxPlot", + "Figure_Title": "Test BoxPlot", "Horizontal_Plot": False, - "Figure_Width": 12, - "Figure_Height": 8, - "Figure_DPI": 300, + "Figure_Width": 6, + "Figure_Height": 4, + "Figure_DPI": 72, # low DPI for fast save "Font_Size": 10, "Keep_Outliers": True, - "Output_File": self.out_file, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "figures": {"type": "directory", "name": "figures"}, + "dataframe": {"type": "file", "name": "output.csv"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - @patch('spac.templates.boxplot_template.boxplot') - @patch('matplotlib.pyplot.show') - def test_run_with_save(self, mock_show, mock_boxplot) -> None: - """Test boxplot with file saving.""" - # Mock the boxplot function to return figure, ax, and dataframe - rng = np.random.default_rng(42) - mock_fig, mock_ax, mock_df = self._create_mock_boxplot_return({ - 'Gene_0': rng.random(20), - 'Gene_1': rng.random(20), - 'Gene_2': rng.random(20) - }) - mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) - - # Suppress warnings for cleaner test output - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test with save_results=True (default) - saved_files = run_from_json(self.params) - self.assertIn(self.out_file, saved_files) - - # Verify boxplot was called with correct parameters - mock_boxplot.assert_called_once() - call_args = mock_boxplot.call_args - - # Check keyword arguments - self.assertEqual(call_args[1]['annotation'], "cell_type") - self.assertEqual(call_args[1]['second_annotation'], None) - self.assertEqual(call_args[1]['layer'], None) # Original -> None - self.assertEqual(call_args[1]['log_scale'], False) - self.assertEqual(call_args[1]['orient'], "v") - self.assertEqual(call_args[1]['showfliers'], True) - - @patch('spac.templates.boxplot_template.boxplot') - @patch('matplotlib.pyplot.show') - def test_run_without_save(self, mock_show, mock_boxplot) -> None: - """Test boxplot without file saving.""" - # Mock the boxplot function - mock_fig, mock_ax, mock_df = self._create_mock_boxplot_return({ - 'Gene_0': [1, 2, 3], - 'Gene_1': [4, 5, 6], - 'Gene_2': [7, 8, 9] - }) - mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) - - # Test with save_results=False - fig, summary_df = run_from_json( - self.params, save_results=False + def test_boxplot_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run boxplot template and verify output + artifacts. + + Validates: + 1. saved_files is a dict with 'figures' and 'dataframe' keys + 2. A figures directory is created containing a non-empty PNG + 3. The figure title matches the "Figure_Title" param + 4. A summary CSV is created with the exact describe() rows + """ + # -- Act (save_to_disk=True): write outputs to disk ------------ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + show_plot=False, # no GUI in CI + output_dir=self.tmp_dir.name, ) - - # Verify we got the figure and summary dataframe back - self.assertEqual(fig, mock_fig) - self.assertIsInstance(summary_df, pd.DataFrame) - # Check that summary has statistics - self.assertIn('count', summary_df['index'].values) - self.assertIn('mean', summary_df['index'].values) - self.assertIn('std', summary_df['index'].values) - - @patch('spac.templates.boxplot_template.boxplot') - @patch('matplotlib.pyplot.show') - def test_all_features_plotting(self, mock_show, mock_boxplot) -> None: - """Test plotting all features.""" - mock_fig, mock_ax, mock_df = self._create_mock_boxplot_return({ - 'Gene_0': [1], 'Gene_1': [2] - }) - mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) - - run_from_json(self.params, save_results=False) - - # Verify features parameter - should be all gene names - call_args = mock_boxplot.call_args - features = call_args[1]['features'] - self.assertEqual(len(features), 3) # mock data has 3 genes - self.assertIn('Gene_0', features) - self.assertIn('Gene_1', features) - self.assertIn('Gene_2', features) - - @patch('spac.templates.boxplot_template.boxplot') - @patch('matplotlib.pyplot.show') - def test_specific_features(self, mock_show, mock_boxplot) -> None: - """Test plotting specific features.""" - params_specific = self.params.copy() - params_specific["Feature_s_to_Plot"] = ["Gene_0", "Gene_2"] - - mock_fig = MagicMock() - mock_ax = MagicMock() - mock_df = pd.DataFrame({'Gene_0': [1], 'Gene_2': [2]}) - mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) - - run_from_json(params_specific, save_results=False) - - # Verify specific features were passed - call_args = mock_boxplot.call_args + + # -- Act (save_to_disk=False): get figure + df in memory ------- + fig, summary_df_mem = run_from_json( + self.json_file, + save_to_disk=False, + show_plot=False, + ) + + # -- Assert: return type --------------------------------------- + self.assertIsInstance( + saved_files, dict, + f"Expected dict from run_from_json, got {type(saved_files)}" + ) + + # -- Assert: figures directory contains at least one PNG ------- + self.assertIn("figures", saved_files, + "Missing 'figures' key in saved_files") + figure_paths = saved_files["figures"] + self.assertGreaterEqual( + len(figure_paths), 1, "No figure files were saved" + ) + + for fig_path in figure_paths: + fig_file = Path(fig_path) + self.assertTrue( + fig_file.exists(), f"Figure not found: {fig_path}" + ) + self.assertGreater( + fig_file.stat().st_size, 0, + f"Figure file is empty: {fig_path}" + ) + # Template saves matplotlib figures as .png + self.assertEqual( + fig_file.suffix, ".png", + f"Expected .png extension, got {fig_file.suffix}" + ) + + # -- Assert: figure has the correct title ---------------------- + # The template calls ax.set_title(figure_title), so the axes + # title must match the "Figure_Title" parameter we passed in. + axes_title = fig.axes[0].get_title() self.assertEqual( - call_args[1]['features'], ["Gene_0", "Gene_2"] + axes_title, "Test BoxPlot", + f"Expected figure title 'Test BoxPlot', got '{axes_title}'" ) - @patch('spac.templates.boxplot_template.boxplot') - @patch('matplotlib.pyplot.show') - def test_layer_selection(self, mock_show, mock_boxplot) -> None: - """Test different layer selections.""" - # Test normalized layer - params_norm = self.params.copy() - params_norm["Table_to_Visualize"] = "normalized" - - mock_fig = MagicMock() - mock_ax = MagicMock() - mock_df = pd.DataFrame({'Gene_0': [1]}) - mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) - - run_from_json(params_norm, save_results=False) - - call_args = mock_boxplot.call_args - self.assertEqual(call_args[1]['layer'], "normalized") - - @patch('spac.templates.boxplot_template.boxplot') - @patch('matplotlib.pyplot.show') - def test_horizontal_orientation(self, mock_show, mock_boxplot) -> None: - """Test horizontal plot orientation.""" - params_horiz = self.params.copy() - params_horiz["Horizontal_Plot"] = True - - mock_fig = MagicMock() - mock_ax = MagicMock() - mock_df = pd.DataFrame({'Gene_0': [1]}) - mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) - - run_from_json(params_horiz, save_results=False) - - call_args = mock_boxplot.call_args - self.assertEqual(call_args[1]['orient'], "h") - - @patch('spac.templates.boxplot_template.boxplot') - @patch('matplotlib.pyplot.show') - def test_secondary_annotation(self, mock_show, mock_boxplot) -> None: - """Test with secondary annotation.""" - params_second = self.params.copy() - params_second["Secondary_Annotation"] = "condition" - - mock_fig = MagicMock() - mock_ax = MagicMock() - mock_df = pd.DataFrame({'Gene_0': [1]}) - mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) - - run_from_json(params_second, save_results=False) - - call_args = mock_boxplot.call_args - self.assertEqual(call_args[1]['second_annotation'], "condition") - - @patch('spac.templates.boxplot_template.boxplot') - @patch('matplotlib.pyplot.show') - def test_log_scale(self, mock_show, mock_boxplot) -> None: - """Test log scale option.""" - params_log = self.params.copy() - params_log["Value_Axis_Log_Scale"] = True - - mock_fig = MagicMock() - mock_ax = MagicMock() - mock_df = pd.DataFrame({'Gene_0': [1]}) - mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) - - run_from_json(params_log, save_results=False) - - call_args = mock_boxplot.call_args - self.assertEqual(call_args[1]['log_scale'], True) - - @patch('spac.templates.boxplot_template.boxplot') - @patch('matplotlib.pyplot.show') - def test_outliers_option(self, mock_show, mock_boxplot) -> None: - """Test outliers (showfliers) option.""" - params_no_outliers = self.params.copy() - params_no_outliers["Keep_Outliers"] = False - - mock_fig = MagicMock() - mock_ax = MagicMock() - mock_df = pd.DataFrame({'Gene_0': [1]}) - mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) - - run_from_json(params_no_outliers, save_results=False) - - call_args = mock_boxplot.call_args - self.assertEqual(call_args[1]['showfliers'], False) - - @patch('spac.templates.boxplot_template.boxplot') - @patch('matplotlib.pyplot.show') - @patch('matplotlib.pyplot.subplots') - def test_figure_parameters(self, mock_subplots, mock_show, mock_boxplot) -> None: - """Test figure configuration parameters.""" - params_fig = self.params.copy() - params_fig.update({ - "Figure_Width": 15, - "Figure_Height": 10, - "Figure_DPI": 150, - "Font_Size": 14 - }) - - # Mock the figure and axes from plt.subplots - mock_fig = MagicMock() - mock_ax = MagicMock() - mock_subplots.return_value = (mock_fig, mock_ax) - - # Mock the boxplot return - mock_df = pd.DataFrame({'Gene_0': [1]}) - mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) - - run_from_json(params_fig, save_results=False) - - # Verify figure methods were called with correct values - mock_fig.set_size_inches.assert_called_with(15, 10) - mock_fig.set_dpi.assert_called_with(150) - - @patch('spac.templates.boxplot_template.boxplot') - @patch('matplotlib.pyplot.show') - @patch('spac.templates.boxplot_template.logging.info') - @patch('spac.templates.boxplot_template.logging.debug') - def test_console_output(self, mock_debug, mock_info, mock_show, mock_boxplot) -> None: - """Test that summary statistics are logged to console.""" - mock_fig, mock_ax, mock_df = self._create_mock_boxplot_return({ - 'Gene_0': [1, 2, 3], - 'Gene_1': [4, 5, 6] - }) - mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) - - run_from_json(self.params, save_results=False) - - # Check that appropriate messages were logged - info_calls = [str(call[0][0]) for call in mock_info.call_args_list - if call[0]] - - # Should log "Plotting All Features" + # -- Assert: summary CSV exists and is non-empty --------------- + self.assertIn("dataframe", saved_files, + "Missing 'dataframe' key in saved_files") + csv_path = Path(saved_files["dataframe"]) self.assertTrue( - any("Plotting All Features" in msg for msg in info_calls) + csv_path.exists(), f"Summary CSV not found: {csv_path}" ) - - # Should log "Summary statistics of the dataset:" - self.assertTrue( - any("Summary statistics" in msg for msg in info_calls) + self.assertGreater( + csv_path.stat().st_size, 0, + f"Summary CSV is empty: {csv_path}" ) - @patch('spac.templates.boxplot_template.boxplot') - @patch('matplotlib.pyplot.show') - def test_json_file_input(self, mock_show, mock_boxplot) -> None: - """Test with JSON file input.""" - mock_fig = MagicMock() - mock_ax = MagicMock() - mock_df = pd.DataFrame({'Gene_0': [1]}) - mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) - - json_path = os.path.join(self.tmp_dir.name, "boxplot_params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result = run_from_json(json_path, save_results=False) - - # Should return tuple when save_results=False - self.assertIsInstance(result, tuple) - self.assertEqual(len(result), 2) - - @patch('spac.templates.boxplot_template.boxplot') - @patch('matplotlib.pyplot.show') - def test_no_annotation(self, mock_show, mock_boxplot) -> None: - """Test with no annotation (None values).""" - params_no_annot = self.params.copy() - params_no_annot["Primary_Annotation"] = "None" - - mock_fig = MagicMock() - mock_ax = MagicMock() - mock_df = pd.DataFrame({'Gene_0': [1]}) - mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) - - run_from_json(params_no_annot, save_results=False) - - call_args = mock_boxplot.call_args - self.assertIsNone(call_args[1]['annotation']) - - @patch('spac.templates.boxplot_template.boxplot') - @patch('matplotlib.pyplot.show') - def test_legend_handling(self, mock_show, mock_boxplot) -> None: - """Test legend positioning handling.""" - mock_fig = MagicMock() - mock_ax = MagicMock() - mock_df = pd.DataFrame({'Gene_0': [1]}) - mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) - - # Test both with and without legend error - with patch('seaborn.move_legend') as mock_move_legend: - # Case 1: Legend exists - run_from_json(self.params, save_results=False) - mock_move_legend.assert_called_once() - - # Case 2: Legend doesn't exist (raises exception) - mock_move_legend.side_effect = Exception("No legend") - run_from_json(self.params, save_results=False) - # Should not raise error, just print message - - def test_parameter_validation(self) -> None: - """Test that missing required parameters raise errors.""" - params_missing = self.params.copy() - del params_missing["Upstream_Analysis"] - - with self.assertRaises(KeyError) as context: - run_from_json(params_missing) - - self.assertIn("Upstream_Analysis", str(context.exception)) - - @patch('spac.templates.boxplot_template.boxplot') - @patch('matplotlib.pyplot.show') - def test_save_figure_option(self, mock_show, mock_boxplot) -> None: - """Test saving figure to file.""" - params_fig_save = self.params.copy() - params_fig_save["Figure_File"] = "boxplot_figure.png" - - mock_fig, mock_ax, mock_df = self._create_mock_boxplot_return() - mock_boxplot.return_value = (mock_fig, mock_ax, mock_df) - - saved_files = run_from_json(params_fig_save) - - # Should save both summary and figure - self.assertIn(self.out_file, saved_files) - self.assertIn("boxplot_figure.png", saved_files) - self.assertEqual(len(saved_files), 2) + # -- Assert: CSV has the exact describe() stat rows ------------ + # The template calls df.describe().reset_index() which produces + # exactly these 8 rows in this order. + summary_df = pd.read_csv(csv_path) + expected_stats = [ + "count", "mean", "std", "min", + "25%", "50%", "75%", "max", + ] + + # First column after reset_index() is called "index" + actual_stats = summary_df["index"].tolist() + self.assertEqual( + actual_stats, expected_stats, + f"Summary CSV stat rows don't match.\n" + f" Expected: {expected_stats}\n" + f" Actual: {actual_stats}" + ) if __name__ == "__main__": diff --git a/tests/templates/test_calculate_centroid_template.py b/tests/templates/test_calculate_centroid_template.py index d3c98595..7093034e 100644 --- a/tests/templates/test_calculate_centroid_template.py +++ b/tests/templates/test_calculate_centroid_template.py @@ -1,18 +1,23 @@ # tests/templates/test_calculate_centroid_template.py -"""Unit tests for the Calculate Centroid template.""" +""" +Real (non-mocked) unit test for the Calculate Centroid template. + +Validates template I/O behaviour only: + - Expected output files are produced on disk + - Filenames follow the convention + - Output artifacts are non-empty + +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os -import pickle import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import pandas as pd -import numpy as np -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -21,35 +26,31 @@ from spac.templates.calculate_centroid_template import run_from_json -def mock_dataframe(n_cells: int = 10) -> pd.DataFrame: - """Return a minimal synthetic DataFrame for fast tests.""" - rng = np.random.default_rng(0) +def _make_tiny_dataframe() -> pd.DataFrame: + """ + Minimal synthetic DataFrame with bounding-box coordinate columns. + + 4 rows -- enough to exercise the centroid calculation. + """ return pd.DataFrame({ - "XMin": rng.uniform(0, 50, n_cells), - "XMax": rng.uniform(50, 100, n_cells), - "YMin": rng.uniform(0, 50, n_cells), - "YMax": rng.uniform(50, 100, n_cells), - "CellID": range(n_cells), - "CellType": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells] + "XMin": [0.0, 10.0, 20.0, 30.0], + "XMax": [10.0, 20.0, 30.0, 40.0], + "YMin": [0.0, 5.0, 10.0, 15.0], + "YMax": [4.0, 9.0, 14.0, 19.0], + "cell_type": ["A", "B", "A", "B"], }) class TestCalculateCentroidTemplate(unittest.TestCase): - """Unit tests for the Calculate Centroid template.""" + """Real (non-mocked) tests for the calculate centroid template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.csv" - ) - self.out_file = "centroid_calculated" + self.in_file = os.path.join(self.tmp_dir.name, "input.csv") - # Save minimal mock data as CSV - mock_df = mock_dataframe() - mock_df.to_csv(self.in_file, index=False) + _make_tiny_dataframe().to_csv(self.in_file, index=False) - # Minimal parameters from JSON template - self.params = { + params = { "Upstream_Dataset": self.in_file, "Min_X_Coordinate_Column_Name": "XMin", "Max_X_Coordinate_Column_Name": "XMax", @@ -57,110 +58,69 @@ def setUp(self) -> None: "Max_Y_Coordinate_Column_Name": "YMax", "X_Centroid_Name": "XCentroid", "Y_Centroid_Name": "YCentroid", - "Output_File": self.out_file, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters (CSV input) - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - # Verify file was saved - self.assertTrue(len(result) > 0) - csv_files = [f for f in result.values() if '.csv' in str(f)] - self.assertTrue(len(csv_files) > 0) - - # Test 2: Run without saving - result_no_save = run_from_json(self.params, save_results=False) - # Check appropriate return type - self.assertIsInstance(result_no_save, pd.DataFrame) - # Verify centroids were calculated - self.assertIn("XCentroid", result_no_save.columns) - self.assertIn("YCentroid", result_no_save.columns) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - # Test 4: Pickle file input - pickle_file = os.path.join(self.tmp_dir.name, "input.pickle") - mock_df = mock_dataframe() - with open(pickle_file, 'wb') as f: - pickle.dump(mock_df, f) - - params_pickle = self.params.copy() - params_pickle["Upstream_Dataset"] = pickle_file - result_pickle = run_from_json(params_pickle, save_results=False) - self.assertIsInstance(result_pickle, pd.DataFrame) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test unsupported file format - params_bad = self.params.copy() - params_bad["Upstream_Dataset"] = "invalid.txt" - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - # Check error message - self.assertIn("Unsupported file format", str(context.exception)) - - @patch('spac.templates.calculate_centroid_template.calculate_centroid') - def test_function_calls(self, mock_calc) -> None: - """Test that main function is called with correct parameters.""" - # Mock the calculate_centroid function - mock_df = mock_dataframe() - mock_df["XCentroid"] = (mock_df["XMin"] + mock_df["XMax"]) / 2 - mock_df["YCentroid"] = (mock_df["YMin"] + mock_df["YMax"]) / 2 - mock_calc.return_value = mock_df - - run_from_json(self.params, save_results=False) - - # Verify function was called correctly - mock_calc.assert_called_once() - call_kwargs = mock_calc.call_args[1] - - # Check specific parameter conversions - self.assertEqual(call_kwargs['x_min'], "XMin") - self.assertEqual(call_kwargs['x_max'], "XMax") - self.assertEqual(call_kwargs['y_min'], "YMin") - self.assertEqual(call_kwargs['y_max'], "YMax") - self.assertEqual(call_kwargs['new_x'], "XCentroid") - self.assertEqual(call_kwargs['new_y'], "YCentroid") - - def test_direct_dataframe_input(self) -> None: - """Test that DataFrame can be passed directly.""" - mock_df = mock_dataframe() - params_df = self.params.copy() - params_df["Upstream_Dataset"] = mock_df - - calc_centroid_patch = ( - 'spac.templates.calculate_centroid_template.calculate_centroid' + def test_calculate_centroid_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run calculate centroid template and verify + output artifacts. + + Validates: + 1. saved_files is a dict with 'dataframe' key + 2. Output CSV exists and is non-empty + 3. Centroid columns are present and correctly computed + """ + # -- Act (save_to_disk=True) ----------------------------------- + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) - with patch(calc_centroid_patch) as mock_calc: - # Mock return value - result_df = mock_df.copy() - result_df["XCentroid"] = 75.0 - result_df["YCentroid"] = 75.0 - mock_calc.return_value = result_df - - result = run_from_json(params_df, save_results=False) - self.assertIsInstance(result, pd.DataFrame) - - # Verify the input DataFrame was passed correctly - call_args = mock_calc.call_args[0] - pd.testing.assert_frame_equal(call_args[0], mock_df) + + # -- Assert: return type --------------------------------------- + self.assertIsInstance(saved_files, dict) + + # -- Assert: CSV file exists and is non-empty ------------------ + self.assertIn("dataframe", saved_files) + csv_path = Path(saved_files["dataframe"]) + self.assertTrue(csv_path.exists(), f"CSV not found: {csv_path}") + self.assertGreater(csv_path.stat().st_size, 0) + + # -- Assert: centroid columns are present and correct ---------- + result_df = pd.read_csv(csv_path) + self.assertIn("XCentroid", result_df.columns) + self.assertIn("YCentroid", result_df.columns) + + # XCentroid = (XMin + XMax) / 2 + expected_x = [5.0, 15.0, 25.0, 35.0] + self.assertEqual(result_df["XCentroid"].tolist(), expected_x) + + # YCentroid = (YMin + YMax) / 2 + expected_y = [2.0, 7.0, 12.0, 17.0] + self.assertEqual(result_df["YCentroid"].tolist(), expected_y) + + # -- Act (save_to_disk=False) ---------------------------------- + mem_df = run_from_json( + self.json_file, + save_to_disk=False, + ) + + # -- Assert: in-memory return is DataFrame --------------------- + self.assertIsInstance(mem_df, pd.DataFrame) + self.assertIn("XCentroid", mem_df.columns) + self.assertIn("YCentroid", mem_df.columns) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_combine_annotations_template.py b/tests/templates/test_combine_annotations_template.py index 76632ee2..a8f91a23 100644 --- a/tests/templates/test_combine_annotations_template.py +++ b/tests/templates/test_combine_annotations_template.py @@ -1,5 +1,10 @@ # tests/templates/test_combine_annotations_template.py -"""Unit tests for the Combine Annotations template.""" +""" +Real (non-mocked) unit test for the Combine Annotations template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,13 +12,11 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -22,189 +25,86 @@ from spac.templates.combine_annotations_template import run_from_json -def mock_adata(n_cells: int = 10) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 4 cells with two annotation columns to combine.""" + rng = np.random.default_rng(42) + X = rng.random((4, 2)) obs = pd.DataFrame({ - "detailed_cell_type": ( - ["B_cell", "T_cell", "NK_cell"] * ((n_cells + 2) // 3) - )[:n_cells], - "broad_cell_type": ( - ["Immune", "Immune", "Immune"] * ((n_cells + 2) // 3) - )[:n_cells] + "tissue": ["lung", "liver", "lung", "liver"], + "cell_type": ["B cell", "T cell", "T cell", "B cell"], }) - x_mat = rng.normal(size=(n_cells, 3)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = ["Gene1", "Gene2", "Gene3"] - return adata + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + return ad.AnnData(X=X, obs=obs, var=var) class TestCombineAnnotationsTemplate(unittest.TestCase): - """Unit tests for the Combine Annotations template.""" + """Real (non-mocked) tests for the combine annotations template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "transform_output" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters matching NIDAP template - self.params = { + params = { "Upstream_Analysis": self.in_file, - "Annotations_Names": ["detailed_cell_type", "broad_cell_type"], - "New_Annotation_Name": "combined_annotation", + "Annotations_Names": ["tissue", "cell_type"], "Separator": "_", - "Output_File": self.out_file, + "New_Annotation_Name": "combined", + "Output_Directory": self.tmp_dir.name, + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"}, + "analysis": {"type": "file", "name": "output.pickle"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering all input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - # Should have 2 files: pickle and CSV - self.assertEqual(len(result), 2) - # Check pickle file exists - self.assertIn( - f"{self.out_file}.pickle", result - ) - # Check CSV file exists - self.assertIn("combined_annotation_counts.csv", result) - - # Test 2: Run without saving - adata_result = run_from_json(self.params, save_results=False) - # Check we got AnnData back - self.assertIsInstance(adata_result, ad.AnnData) - # Check new annotation was created - self.assertIn("combined_annotation", adata_result.obs.columns) - # Check annotation values are correct format - sample_val = adata_result.obs["combined_annotation"].iloc[0] - self.assertIn("_", sample_val) # Should have separator - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - self.assertEqual(len(result_json), 2) - - # Test 4: Verify CSV content - csv_path = result_json["combined_annotation_counts.csv"] - df_counts = pd.read_csv(csv_path) - # Should have the annotation name and count columns - self.assertIn("combined_annotation", df_counts.columns) - self.assertIn("count", df_counts.columns) - # Should have some rows - self.assertGreater(len(df_counts), 0) - - def test_parameter_validation(self) -> None: - """Test exact error message for missing parameters.""" - params_bad = self.params.copy() - del params_bad["Annotations_Names"] - - with self.assertRaises(KeyError) as context: - run_from_json(params_bad) - - # Check that KeyError mentions the missing parameter - self.assertIn("Annotations_Names", str(context.exception)) - - @patch('spac.templates.combine_annotations_template.combine_annotations') - def test_function_calls(self, mock_combine) -> None: - """Test that main function is called with correct parameters.""" - # Mock the combine_annotations function to add the expected column - def mock_combine_side_effect(adata, **kwargs): - # Simulate what combine_annotations does - annotations = kwargs.get('annotations', []) - separator = kwargs.get('separator', '_') - new_name = kwargs.get('new_annotation_name', 'combined') - - # Create combined values - combined_values = [] - for idx in range(len(adata.obs)): - values = [str(adata.obs[col].iloc[idx]) for col in annotations] - combined_values.append(separator.join(values)) - - adata.obs[new_name] = combined_values - return None - - mock_combine.side_effect = mock_combine_side_effect - - run_from_json(self.params) - - # Verify function was called correctly - mock_combine.assert_called_once() - call_args = mock_combine.call_args - - # Check positional arguments (adata) - self.assertEqual(call_args[0][0].n_obs, 10) - - # Check keyword arguments - self.assertEqual( - call_args[1]['annotations'], - ["detailed_cell_type", "broad_cell_type"] - ) - self.assertEqual(call_args[1]['separator'], "_") - self.assertEqual( - call_args[1]['new_annotation_name'], - "combined_annotation" + def test_combine_annotations_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run combine annotations and verify outputs. + + Validates: + 1. saved_files dict has 'analysis' and 'dataframe' keys + 2. Pickle contains AnnData with 'combined' obs column + 3. CSV exists and is non-empty + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) - def test_different_separators(self) -> None: - """Test with different separator characters.""" - # Test with hyphen separator - params_hyphen = self.params.copy() - params_hyphen["Separator"] = "-" - params_hyphen["New_Annotation_Name"] = "hyphen_combined" - - adata = run_from_json(params_hyphen, save_results=False) - self.assertIn("hyphen_combined", adata.obs.columns) - # Check that values use hyphen - sample_val = adata.obs["hyphen_combined"].iloc[0] - self.assertIn("-", sample_val) - - # Test with empty separator - params_empty = self.params.copy() - params_empty["Separator"] = "" - params_empty["New_Annotation_Name"] = "concat_combined" - - adata = run_from_json(params_empty, save_results=False) - self.assertIn("concat_combined", adata.obs.columns) - - @patch('builtins.print') - def test_console_output(self, mock_print) -> None: - """Test that expected console output is produced.""" - run_from_json(self.params, save_results=False) - - # Check that print was called with expected messages - print_calls = [str(call[0][0]) for call in mock_print.call_args_list - if call[0]] - - # Should print after combining annotations - after_combining = any( - "After combining annotations:" in msg for msg in print_calls - ) - self.assertTrue(after_combining) - - # Should print unique labels message - unique_labels = any( - "Unique labels in combined_annotation" in msg - for msg in print_calls - ) - self.assertTrue(unique_labels) + self.assertIsInstance(saved_files, dict) + self.assertIn("analysis", saved_files) + self.assertIn("dataframe", saved_files) + + # -- Pickle output -- + pkl_path = Path(saved_files["analysis"]) + self.assertTrue(pkl_path.exists()) + self.assertGreater(pkl_path.stat().st_size, 0) + + with open(pkl_path, "rb") as f: + result_adata = pickle.load(f) + self.assertIsInstance(result_adata, ad.AnnData) + self.assertIn("combined", result_adata.obs.columns) + + # -- CSV output -- + csv_path = Path(saved_files["dataframe"]) + self.assertTrue(csv_path.exists()) + self.assertGreater(csv_path.stat().st_size, 0) + + # -- In-memory -- + mem_adata = run_from_json(self.json_file, save_to_disk=False) + self.assertIsInstance(mem_adata, ad.AnnData) + self.assertIn("combined", mem_adata.obs.columns) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_combine_dataframes_template.py b/tests/templates/test_combine_dataframes_template.py index 7a62a38b..6942eb78 100644 --- a/tests/templates/test_combine_dataframes_template.py +++ b/tests/templates/test_combine_dataframes_template.py @@ -1,17 +1,23 @@ # tests/templates/test_combine_dataframes_template.py -"""Unit tests for the Combine DataFrames template.""" +""" +Real (non-mocked) unit test for the Combine DataFrames template. + +Validates template I/O behaviour only: + - Expected output files are produced on disk + - Filenames follow the convention + - Output artifacts are non-empty + +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os -import pickle import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -20,141 +26,89 @@ from spac.templates.combine_dataframes_template import run_from_json -def mock_dataframe(n_rows: int = 10, prefix: str = "") -> pd.DataFrame: - """Return a minimal synthetic DataFrame for fast tests.""" - return pd.DataFrame({ - f'{prefix}col1': range(n_rows), - f'{prefix}col2': [f'val_{i}' for i in range(n_rows)], - f'{prefix}col3': [i * 2.5 for i in range(n_rows)] +def _make_tiny_dataframes(): + """Two minimal DataFrames with the same schema for concatenation.""" + df_a = pd.DataFrame({ + "cell_type": ["B cell", "T cell"], + "marker": [1.0, 2.0], }) + df_b = pd.DataFrame({ + "cell_type": ["NK cell", "Monocyte"], + "marker": [3.0, 4.0], + }) + return df_a, df_b class TestCombineDataFramesTemplate(unittest.TestCase): - """Unit tests for the Combine DataFrames template.""" + """Real (non-mocked) tests for the combine dataframes template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - - # Create test CSV files - self.csv_file1 = os.path.join( - self.tmp_dir.name, "dataframe1.csv" - ) - self.csv_file2 = os.path.join( - self.tmp_dir.name, "dataframe2.csv" - ) - - # Create test pickle files - self.pkl_file1 = os.path.join( - self.tmp_dir.name, "dataframe1.pkl" - ) - self.pkl_file2 = os.path.join( - self.tmp_dir.name, "dataframe2.pkl" - ) - - # Save test dataframes - df1 = mock_dataframe(10, 'A_') - df2 = mock_dataframe(15, 'B_') - - df1.to_csv(self.csv_file1, index=False) - df2.to_csv(self.csv_file2, index=False) - - with open(self.pkl_file1, 'wb') as f: - pickle.dump(df1, f) - with open(self.pkl_file2, 'wb') as f: - pickle.dump(df2, f) - - self.out_file = "combined_output.csv" - - # Minimal parameters - self.params = { - "First_Dataframe": self.csv_file1, - "Second_Dataframe": self.csv_file2, - "Output_File": self.out_file, + + df_a, df_b = _make_tiny_dataframes() + self.file_a = os.path.join(self.tmp_dir.name, "first.csv") + self.file_b = os.path.join(self.tmp_dir.name, "second.csv") + df_a.to_csv(self.file_a, index=False) + df_b.to_csv(self.file_b, index=False) + + params = { + "First_Dataframe": self.file_a, + "Second_Dataframe": self.file_b, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with CSV files - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - self.assertIn(self.out_file, result) - - # Verify combined file exists and has correct shape - combined_df = pd.read_csv(result[self.out_file]) - self.assertEqual(len(combined_df), 25) # 10 + 15 rows - self.assertEqual(len(combined_df.columns), 6) # 3 + 3 columns - - # Test 2: Run with pickle files - params_pkl = self.params.copy() - params_pkl["First_Dataframe"] = self.pkl_file1 - params_pkl["Second_Dataframe"] = self.pkl_file2 - - result_pkl = run_from_json(params_pkl) - self.assertIsInstance(result_pkl, dict) - - # Test 3: Run without saving - result_no_save = run_from_json(self.params, save_results=False) - self.assertIsInstance(result_no_save, pd.DataFrame) - self.assertEqual(len(result_no_save), 25) - - # Test 4: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test unsupported file format - params_bad = self.params.copy() - params_bad["First_Dataframe"] = "file.txt" - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - # Check error message contains expected text - self.assertIn("Unsupported file format", str(context.exception)) - self.assertIn(".txt", str(context.exception)) - - @patch('spac.templates.combine_dataframes_template.combine_dfs') - def test_function_calls(self, mock_combine) -> None: - """Test that main function is called with correct parameters.""" - # Mock the combine_dfs function - mock_combine.return_value = mock_dataframe(25, 'combined_') - - run_from_json(self.params) - - # Verify function was called correctly - mock_combine.assert_called_once() - # Check that two dataframes were passed - call_args = mock_combine.call_args[0][0] - self.assertEqual(len(call_args), 2) - self.assertIsInstance(call_args[0], pd.DataFrame) - self.assertIsInstance(call_args[1], pd.DataFrame) - - def test_direct_dataframe_input(self) -> None: - """Test passing DataFrames directly instead of file paths.""" - df1 = mock_dataframe(5, 'X_') - df2 = mock_dataframe(8, 'Y_') - - params_df = { - "First_Dataframe": df1, - "Second_Dataframe": df2, - "Output_File": "direct_df_output.csv" - } - - result = run_from_json(params_df, save_results=False) - self.assertIsInstance(result, pd.DataFrame) - self.assertEqual(len(result), 13) # 5 + 8 rows + def test_combine_dataframes_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run combine dataframes template and verify + output artifacts. + + Validates: + 1. saved_files is a dict with 'dataframe' key + 2. Output CSV exists and is non-empty + 3. Combined DataFrame has all rows from both inputs + """ + # -- Act (save_to_disk=True) ----------------------------------- + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, + ) + + # -- Assert: return type --------------------------------------- + self.assertIsInstance(saved_files, dict) + + # -- Assert: CSV file exists and is non-empty ------------------ + self.assertIn("dataframe", saved_files) + csv_path = Path(saved_files["dataframe"]) + self.assertTrue(csv_path.exists(), f"CSV not found: {csv_path}") + self.assertGreater(csv_path.stat().st_size, 0) + + # -- Assert: combined row count -------------------------------- + result_df = pd.read_csv(csv_path) + self.assertEqual(len(result_df), 4) + expected_types = {"B cell", "T cell", "NK cell", "Monocyte"} + self.assertEqual(set(result_df["cell_type"]), expected_types) + + # -- Act (save_to_disk=False) ---------------------------------- + mem_df = run_from_json( + self.json_file, + save_to_disk=False, + ) + + # -- Assert: in-memory return is DataFrame --------------------- + self.assertIsInstance(mem_df, pd.DataFrame) + self.assertEqual(len(mem_df), 4) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_downsample_cells_template.py b/tests/templates/test_downsample_cells_template.py index f607b417..7f76d5b3 100644 --- a/tests/templates/test_downsample_cells_template.py +++ b/tests/templates/test_downsample_cells_template.py @@ -1,18 +1,23 @@ # tests/templates/test_downsample_cells_template.py -"""Unit tests for the Downsample Cells template.""" +""" +Real (non-mocked) unit test for the Downsample Cells template. + +Validates template I/O behaviour only: + - Expected output files are produced on disk + - Filenames follow the convention + - Output artifacts are non-empty + +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os -import pickle import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import pandas as pd -import numpy as np -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -21,141 +26,91 @@ from spac.templates.downsample_cells_template import run_from_json -def mock_dataframe(n_rows: int = 1000) -> pd.DataFrame: - """Return a minimal synthetic DataFrame for fast tests.""" - rng = np.random.default_rng(0) - - # Create a dataframe with multiple annotations for downsampling - data = { - "cell_id": range(n_rows), - "region": rng.choice(["region1", "region2", "region3"], n_rows), - "day": rng.choice(["day1", "day2", "day3", "day4"], n_rows), - "cell_type": rng.choice(["TypeA", "TypeB", "TypeC"], n_rows), - "marker1": rng.normal(100, 15, n_rows), - "marker2": rng.normal(50, 10, n_rows), - } - - return pd.DataFrame(data) +def _make_tiny_dataframe() -> pd.DataFrame: + """ + Minimal synthetic DataFrame for downsampling. + + 8 rows, 2 groups of 4 -- enough to exercise group-based downsampling. + """ + return pd.DataFrame({ + "cell_type": ["A", "A", "A", "A", "B", "B", "B", "B"], + "marker": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], + }) class TestDownsampleCellsTemplate(unittest.TestCase): - """Unit tests for the Downsample Cells template.""" + """Real (non-mocked) tests for the downsample cells template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.csv" - ) - self.out_file = "downsampled_data" + self.in_file = os.path.join(self.tmp_dir.name, "input.csv") - # Save minimal mock data as CSV - mock_df = mock_dataframe() - mock_df.to_csv(self.in_file, index=False) + _make_tiny_dataframe().to_csv(self.in_file, index=False) - # Minimal parameters from NIDAP template - self.params = { + params = { "Upstream_Dataset": self.in_file, - "Annotations_List": ["region", "day"], - "Number_of_Samples": 100, + "Annotations_List": ["cell_type"], + "Number_of_Samples": 2, "Stratify_Option": False, - "Random_Selection": True, - "New_Combined_Annotation_Name": "_combined_", - "Minimum_Threshold": 5, - "Output_File": self.out_file, + "Random_Selection": False, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - - # Verify file was saved with correct extension - self.assertTrue(len(result) > 0) - saved_file = list(result.values())[0] - self.assertTrue(saved_file.endswith('.csv')) - - # Test 2: Run without saving - result_no_save = run_from_json(self.params, save_results=False) - # Check appropriate return type - self.assertIsInstance(result_no_save, pd.DataFrame) - # Verify downsampling occurred - self.assertLessEqual(len(result_no_save), 1000) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - def test_different_input_formats(self) -> None: - """Test loading from different file formats.""" - # Test pickle input - pickle_file = os.path.join(self.tmp_dir.name, "input.pickle") - mock_df = mock_dataframe(500) - with open(pickle_file, 'wb') as f: - pickle.dump(mock_df, f) - - params_pickle = self.params.copy() - params_pickle["Upstream_Dataset"] = pickle_file - - result = run_from_json(params_pickle, save_results=False) - self.assertIsInstance(result, pd.DataFrame) - - # Test direct DataFrame input - params_df = self.params.copy() - params_df["Upstream_Dataset"] = mock_dataframe(300) - - result = run_from_json(params_df, save_results=False) - self.assertIsInstance(result, pd.DataFrame) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test unsupported file format - params_bad = self.params.copy() - params_bad["Upstream_Dataset"] = "data.xlsx" - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - # Check error message contains expected text - self.assertIn("Unsupported file format", str(context.exception)) - self.assertIn(".xlsx", str(context.exception)) - - @patch('spac.templates.downsample_cells_template.downsample_cells') - def test_function_calls(self, mock_downsample) -> None: - """Test that main function is called with correct parameters.""" - # Mock the downsample function - mock_downsample.return_value = pd.DataFrame({"col": [1, 2, 3]}) - - # Test with stratify option - params_stratify = self.params.copy() - params_stratify["Stratify_Option"] = True - params_stratify["Random_Selection"] = False - params_stratify["Number_of_Samples"] = 50 - - run_from_json(params_stratify, save_results=False) - - # Verify function was called correctly - mock_downsample.assert_called_once() - call_args = mock_downsample.call_args - - # Check specific parameter conversions - self.assertEqual(call_args[1]['annotations'], ["region", "day"]) - self.assertEqual(call_args[1]['n_samples'], 50) - self.assertEqual(call_args[1]['stratify'], True) - self.assertEqual(call_args[1]['rand'], False) - self.assertEqual(call_args[1]['combined_col_name'], "_combined_") - self.assertEqual(call_args[1]['min_threshold'], 5) + def test_downsample_cells_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run downsample cells template and verify + output artifacts. + + Validates: + 1. saved_files is a dict with 'dataframe' key + 2. Output CSV exists and is non-empty + 3. Row count is reduced (2 per group = 4 total from 8) + """ + # -- Act (save_to_disk=True) ----------------------------------- + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, + ) + + # -- Assert: return type --------------------------------------- + self.assertIsInstance(saved_files, dict) + + # -- Assert: CSV file exists and is non-empty ------------------ + self.assertIn("dataframe", saved_files) + csv_path = Path(saved_files["dataframe"]) + self.assertTrue(csv_path.exists(), f"CSV not found: {csv_path}") + self.assertGreater(csv_path.stat().st_size, 0) + + # -- Assert: downsampled row count ----------------------------- + result_df = pd.read_csv(csv_path) + # 2 samples per group * 2 groups = 4 rows + self.assertEqual(len(result_df), 4) + # Both groups should still be present + self.assertEqual( + set(result_df["cell_type"].unique()), {"A", "B"} + ) + + # -- Act (save_to_disk=False) ---------------------------------- + mem_df = run_from_json( + self.json_file, + save_to_disk=False, + ) + + # -- Assert: in-memory return is DataFrame --------------------- + self.assertIsInstance(mem_df, pd.DataFrame) + self.assertEqual(len(mem_df), 4) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_hierarchical_heatmap_template.py b/tests/templates/test_hierarchical_heatmap_template.py index fac7d38d..1964b9a0 100644 --- a/tests/templates/test_hierarchical_heatmap_template.py +++ b/tests/templates/test_hierarchical_heatmap_template.py @@ -1,5 +1,10 @@ # tests/templates/test_hierarchical_heatmap_template.py -"""Unit tests for the Hierarchical Heatmap template.""" +""" +Real (non-mocked) unit test for the Hierarchical Heatmap template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,16 +12,14 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import matplotlib -matplotlib.use("Agg") # Headless backend for CI +matplotlib.use("Agg") import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -25,185 +28,79 @@ from spac.templates.hierarchical_heatmap_template import run_from_json -def mock_adata(n_cells: int = 100) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 8 cells, 3 genes, 2 groups for heatmap.""" + rng = np.random.default_rng(42) + X = rng.integers(1, 20, size=(8, 3)).astype(float) obs = pd.DataFrame({ - "phenograph_k60_r1": ( - ["Cluster1", "Cluster2", "Cluster3"] * ((n_cells + 2) // 3) - )[:n_cells], - "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells] + "cell_type": ["A", "A", "B", "B", "A", "A", "B", "B"], }) - # Create expression data with 9 markers as in the example - x_mat = rng.normal(size=(n_cells, 9)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = [ - "Hif1a", "NOS2", "COX2", "β-catenin", "vimentin", - "E-cadherin", "Ki67", "PIMO", "aSMA" - ] - # Add a z-score normalized layer for testing - adata.layers["arcsinh_z_scores"] = ( - (x_mat - x_mat.mean(axis=0)) / x_mat.std(axis=0) - ) - return adata + var = pd.DataFrame(index=["Gene_0", "Gene_1", "Gene_2"]) + return ad.AnnData(X=X, obs=obs, var=var) class TestHierarchicalHeatmapTemplate(unittest.TestCase): - """Unit tests for the Hierarchical Heatmap template.""" + """Real (non-mocked) tests for the hierarchical heatmap template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "mean_intensity.csv" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters - from the JSON template - self.params = { + params = { "Upstream_Analysis": self.in_file, - "Annotation": "phenograph_k60_r1", - "Table_to_Visualize": "arcsinh_z_scores", - "Feature_s_": ["All"], - "Standard_Scale_": "None", - "Z_Score": "None", - "Feature_Dendrogram": True, - "Annotation_Dendrogram": True, - "Figure_Title": "Hierarchical Heatmap", - "Figure_Width": 15, - "Figure_Height": 12, - "Figure_DPI": 300, - "Font_Size": 14, - "Matrix_Plot_Ratio": 0.8, - "Swap_Axes": False, - "Rotate_Label_": False, - "Horizontal_Dendrogram_Display_Ratio": 0.2, - "Vertical_Dendrogram_Display_Ratio": 0.2, - "Value_Min": "-3", - "Value_Max": "3", - "Color_Map": "seismic", - "Output_File": self.out_file, + "Annotation": "cell_type", + "Table_to_Visualize": "Original", + "Features_to_Visualize": ["All"], + "Standard_Scale": "None", + "Method": "average", + "Metric": "euclidean", + "Figure_Width": 6, + "Figure_Height": 4, + "Figure_DPI": 72, + "Font_Size": 8, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "figures": {"type": "directory", "name": "figures_dir"}, + "dataframe": {"type": "file", "name": "dataframe.csv"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Mock the hierarchical_heatmap function - mock_clustergrid = MagicMock() - mock_clustergrid.fig = MagicMock() - mock_clustergrid.ax_heatmap = MagicMock() - mock_clustergrid.height = 12 - mock_clustergrid.width = 15 - - mock_mean_intensity = pd.DataFrame({ - 'Hif1a': [0.1, 0.2, 0.3], - 'NOS2': [0.4, 0.5, 0.6], - 'COX2': [0.7, 0.8, 0.9] - }, index=['Cluster1', 'Cluster2', 'Cluster3']) - - mock_dendrogram_data = { - 'row_dendrogram': {'data': 'row_data'}, - 'col_dendrogram': {'data': 'col_data'} - } - - with patch( - 'spac.templates.hierarchical_heatmap_template.' - 'hierarchical_heatmap', - return_value=( - mock_mean_intensity, mock_clustergrid, - mock_dendrogram_data - ) - ): - - # Test 1: Run with default parameters - result = run_from_json(self.params, show_plot=False) - self.assertIsInstance(result, dict) - self.assertIn(self.out_file, result) - - # Test 2: Run without saving - result_no_save = run_from_json( - self.params, save_results=False, show_plot=False - ) - # Check appropriate return type based on template - self.assertIsInstance(result_no_save, pd.DataFrame) - self.assertEqual(len(result_no_save), 3) # 3 clusters - - # Test 3: JSON file input - json_path = os.path.join( - self.tmp_dir.name, "params.json" - ) - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path, show_plot=False) - self.assertIsInstance(result_json, dict) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test invalid standard scale conversion - params_bad = self.params.copy() - params_bad["Standard_Scale_"] = "invalid_number" - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad, show_plot=False) - - # Check exact error message - expected_msg = ( - "Error: can't convert Standard Scale to integer. " - "Received:\"invalid_number\"" + def test_hierarchical_heatmap_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run hierarchical heatmap and verify outputs. + + Validates: + 1. saved_files dict has 'figures' and 'dataframe' keys + 2. Figures directory contains non-empty PNG(s) + 3. Summary CSV exists + """ + saved_files = run_from_json( + self.json_file, + save_results_flag=True, + show_plot=False, + output_dir=self.tmp_dir.name, ) - self.assertEqual(str(context.exception), expected_msg) - - @patch('spac.templates.hierarchical_heatmap_template.' - 'hierarchical_heatmap') - def test_function_calls(self, mock_heatmap) -> None: - """Test that main function is called with correct parameters.""" - # Mock the main function - mock_clustergrid = MagicMock() - mock_clustergrid.fig = MagicMock() - mock_clustergrid.ax_heatmap = MagicMock() - - # Create a proper dataframe with matching dimensions - mock_mean_intensity = pd.DataFrame({ - 'Hif1a': [0.1, 0.2, 0.3], - 'NOS2': [0.4, 0.5, 0.6], - 'COX2': [0.7, 0.8, 0.9] - }, index=['Cluster1', 'Cluster2', 'Cluster3']) - - mock_heatmap.return_value = ( - mock_mean_intensity, mock_clustergrid, {} - ) - - # Test with swap_axes=True to verify features handling - params_swap = self.params.copy() - params_swap["Swap_Axes"] = True - params_swap["Feature_s_"] = ["Hif1a", "NOS2"] - - run_from_json(params_swap, save_results=False, show_plot=False) - - # Verify function was called correctly - mock_heatmap.assert_called_once() - call_kwargs = mock_heatmap.call_args[1] - - # Check specific parameter conversions - self.assertEqual( - call_kwargs['annotation'], "phenograph_k60_r1" - ) - self.assertEqual(call_kwargs['layer'], "arcsinh_z_scores") - self.assertEqual(call_kwargs['features'], ["Hif1a", "NOS2"]) - self.assertEqual(call_kwargs['swap_axes'], True) - self.assertEqual(call_kwargs['vmin'], -3.0) - self.assertEqual(call_kwargs['vmax'], 3.0) - self.assertEqual(call_kwargs['cmap'], "seismic") + + self.assertIsInstance(saved_files, dict) + self.assertIn("figures", saved_files) + + figure_paths = saved_files["figures"] + self.assertGreaterEqual(len(figure_paths), 1) + for fig_path in figure_paths: + fig_file = Path(fig_path) + self.assertTrue(fig_file.exists()) + self.assertGreater(fig_file.stat().st_size, 0) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_histogram_template.py b/tests/templates/test_histogram_template.py index e64603c7..5a8e49e8 100644 --- a/tests/templates/test_histogram_template.py +++ b/tests/templates/test_histogram_template.py @@ -1,5 +1,10 @@ # tests/templates/test_histogram_template.py -"""Unit tests for the Histogram template.""" +""" +Real (non-mocked) unit test for the Histogram template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,16 +12,14 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import matplotlib -matplotlib.use("Agg") # Headless backend for CI +matplotlib.use("Agg") import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -25,199 +28,84 @@ from spac.templates.histogram_template import run_from_json -def mock_adata_with_features(n_cells: int = 10) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) - - # Simple expression data - x_mat = rng.normal(size=(n_cells, 3)) - - # Simple observations- fixed to handle odd n_cells - obs = pd.DataFrame({ - "cell_type": (["TypeA", "TypeB"] * (n_cells // 2) + - ["TypeA"] * (n_cells % 2))[:n_cells] - }) - - # Simple var names - var = pd.DataFrame(index=["Gene1", "Gene2", "Gene3"]) - - return ad.AnnData(X=x_mat, obs=obs, var=var) +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 4 cells, 2 genes for histogram plotting.""" + rng = np.random.default_rng(42) + X = rng.integers(1, 10, size=(4, 2)).astype(float) + obs = pd.DataFrame({"cell_type": ["A", "B", "A", "B"]}) + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + return ad.AnnData(X=X, obs=obs, var=var) class TestHistogramTemplate(unittest.TestCase): - """Unit tests for the Histogram template.""" + """Real (non-mocked) tests for the histogram template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "plots.csv" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data as pickle - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata_with_features(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters - self.params = { + params = { "Upstream_Analysis": self.in_file, - "Plot_By": "Annotation", "Annotation": "cell_type", - "Feature": "None", - "Table_": "Original", - "Group_by": "None", - "Together": True, - "Output_File": self.out_file, + "Table_to_Visualize": "Original", + "Feature_s_to_Plot": ["All"], + "Figure_Title": "Test Histogram", + "Legend_Title": "Cell Type", + "Figure_Width": 6, + "Figure_Height": 4, + "Figure_DPI": 72, + "Font_Size": 10, + "Number_of_Bins": 20, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"}, + "figures": {"type": "directory", "name": "figures_dir"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - @patch('spac.templates.histogram_template.histogram') - @patch('seaborn.move_legend') # Mock seaborn to avoid legend issues - def test_run_with_save(self, mock_move_legend, mock_histogram) -> None: - """Test basic run with file saving.""" - # Mock the histogram function - mock_fig = MagicMock() - mock_fig.number = 1 - mock_fig.set_size_inches = MagicMock() - mock_fig.set_dpi = MagicMock() - - mock_ax = MagicMock() - mock_ax.get_legend.return_value = None - mock_ax.tick_params = MagicMock() - mock_ax.set_title = MagicMock() - - mock_df = pd.DataFrame({ - 'category': ['TypeA', 'TypeB'], - 'count': [5, 5] - }) - - mock_histogram.return_value = { - "fig": mock_fig, - "axs": mock_ax, - "df": mock_df - } - - # Run with save_results=True (default) - saved_files = run_from_json(self.params) - - # Check that file was saved - self.assertIn(self.out_file, saved_files) - self.assertTrue(os.path.exists(saved_files[self.out_file])) - - # Verify histogram was called - mock_histogram.assert_called_once() - - @patch('spac.templates.histogram_template.histogram') - @patch('seaborn.move_legend') - def test_run_without_save(self, mock_move_legend, mock_histogram) -> None: - """Test run without file saving.""" - # Mock the histogram function - mock_fig = MagicMock() - mock_fig.number = 1 - mock_fig.set_size_inches = MagicMock() - mock_fig.set_dpi = MagicMock() - - mock_ax = MagicMock() - mock_ax.get_legend.return_value = None - mock_ax.tick_params = MagicMock() - mock_ax.set_title = MagicMock() - - mock_df = pd.DataFrame({'category': ['A'], 'count': [10]}) - - mock_histogram.return_value = { - "fig": mock_fig, - "axs": mock_ax, - "df": mock_df - } - - # Run with save_results=False - fig, df = run_from_json(self.params, save_results=False) - - # Check that we got figure and dataframe - self.assertEqual(fig, mock_fig) - self.assertIsInstance(df, pd.DataFrame) - - def test_json_file_input(self) -> None: - """Test that JSON file input works.""" - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - with patch('spac.templates.histogram_template.histogram') as mock_hist: - with patch('seaborn.move_legend'): - mock_hist.return_value = { - "fig": MagicMock(number=1), - "axs": MagicMock(), - "df": pd.DataFrame() - } - - result = run_from_json(json_path, save_results=False) - self.assertIsInstance(result, tuple) - - def test_error_messages(self) -> None: - """Test exact error messages for key validations.""" - # Test 1: No annotations available - adata_no_obs = ad.AnnData(X=np.random.rand(5, 3)) - with open(self.in_file, 'wb') as f: - pickle.dump(adata_no_obs, f) - - params_bad = self.params.copy() - params_bad["Annotation"] = "None" - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - expected_msg = 'No annotations available in adata.obs to plot.' - self.assertEqual(str(context.exception), expected_msg) - - # Test 2: Invalid rotation angle - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata_with_features(), f) - - params_bad_rotate = self.params.copy() - params_bad_rotate["X_Axis_Label_Rotation"] = 400 - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad_rotate) - - expected_msg = ( - 'The X label rotation should fall within 0 to 360 degree. ' - 'Received "400".' - ) - self.assertEqual(str(context.exception), expected_msg) - - @patch('spac.templates.histogram_template.histogram') - @patch('seaborn.move_legend') - @patch('builtins.print') - def test_console_output( - self, mock_print, mock_move_legend, mock_histogram - ) -> None: - """Test that dataframe is printed to console.""" - # Mock the histogram function - mock_df = pd.DataFrame({ - 'category': ['TypeA', 'TypeB'], - 'count': [5, 5] - }) - - mock_histogram.return_value = { - "fig": MagicMock(number=1), - "axs": MagicMock(), - "df": mock_df - } - - run_from_json(self.params, save_results=False) - - # Check that dataframe info was printed - print_calls = [str(call[0][0]) for call in mock_print.call_args_list - if call[0]] - - # Should print "Displaying top 10 rows" - self.assertTrue( - any("Displaying top 10 rows" in msg for msg in print_calls) + def test_histogram_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run histogram and verify outputs. + + Validates: + 1. saved_files dict has 'figures' and 'dataframe' keys + 2. Figures directory contains non-empty PNG(s) + 3. Summary CSV exists and is non-empty + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + show_plot=False, + output_dir=self.tmp_dir.name, ) + self.assertIsInstance(saved_files, dict) + self.assertIn("figures", saved_files) + self.assertIn("dataframe", saved_files) + + # Figures + figure_paths = saved_files["figures"] + self.assertGreaterEqual(len(figure_paths), 1) + for fig_path in figure_paths: + fig_file = Path(fig_path) + self.assertTrue(fig_file.exists()) + self.assertGreater(fig_file.stat().st_size, 0) + + # CSV + csv_path = Path(saved_files["dataframe"]) + self.assertTrue(csv_path.exists()) + self.assertGreater(csv_path.stat().st_size, 0) + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_interactive_spatial_plot_template.py b/tests/templates/test_interactive_spatial_plot_template.py index 709eb345..ecb9e8f4 100644 --- a/tests/templates/test_interactive_spatial_plot_template.py +++ b/tests/templates/test_interactive_spatial_plot_template.py @@ -1,5 +1,10 @@ # tests/templates/test_interactive_spatial_plot_template.py -"""Unit tests for the Interactive Spatial Plot template.""" +""" +Real (non-mocked) unit test for the Interactive Spatial Plot template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,13 +12,11 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -22,179 +25,73 @@ from spac.templates.interactive_spatial_plot_template import run_from_json -def mock_adata(n_cells: int = 10) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 8 cells with spatial coords.""" + rng = np.random.default_rng(42) + X = rng.random((8, 2)) obs = pd.DataFrame({ - "renamed_phenotypes": ( - ["TypeA", "TypeB"] * ((n_cells + 1) // 2) - )[:n_cells], - "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] + "cell_type": ["A", "B", "A", "B", "A", "B", "A", "B"], }) - x_mat = rng.normal(size=(n_cells, 3)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = ["Gene1", "Gene2", "Gene3"] - # Add spatial coordinates required for spatial plots - adata.obsm["spatial"] = rng.random((n_cells, 2)) * 100 - # Add color mapping - adata.uns["_spac_colors"] = { - "TypeA": "#FF0000", - "TypeB": "#0000FF" - } + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + spatial = rng.random((8, 2)) * 100 + adata = ad.AnnData(X=X, obs=obs, var=var) + adata.obsm["spatial"] = spatial return adata class TestInteractiveSpatialPlotTemplate(unittest.TestCase): - """Unit tests for the Interactive Spatial Plot template.""" + """Real (non-mocked) tests for the interactive spatial plot template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "interactive_plot" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters - adjust based on template - self.params = { + params = { "Upstream_Analysis": self.in_file, "Color_By": "Annotation", - "Annotation_s_to_Highlight": ["renamed_phenotypes"], + "Annotation_s_to_Highlight": ["cell_type"], "Feature_to_Highlight": "None", - "Table": "Original", - "Dot_Size": 3, - "Dot_Transparency": 0.75, - "Feature_Color_Scale": "balance", - "Figure_Width": 12, - "Figure_Height": 12, - "Figure_DPI": 200, - "Font_Size": 12, - "Stratify_By": "None", - "Define_Label_Color_Mapping": "_spac_colors", - "Lower_Colorbar_Bound": 999, - "Upper_Colorbar_Bound": -999, - "Flip_Vertical_Axis": False, - "Output_File": self.out_file, + "Dot_Size": 5, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "html": {"type": "directory", "name": "html_dir"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - @patch('spac.templates.interactive_spatial_plot_template.' - 'interactive_spatial_plot') - @patch('plotly.io.to_html') - def test_complete_io_workflow( - self, - mock_to_html, - mock_spatial_plot, - ) -> None: - """Single I/O test covering input/output scenarios.""" - # Mock the interactive_spatial_plot function - mock_fig1 = MagicMock() - mock_fig1.show = MagicMock() - - mock_spatial_plot.return_value = [ - { - 'image_name': 'plot_1', - 'image_object': mock_fig1 - } - ] - - # Mock HTML conversion - mock_to_html.return_value = "Mock Plot" - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - self.assertEqual(len(result), 1) - expected_file = f"{self.out_file}_plot_1.html" - self.assertIn(expected_file, result) - - # Test 2: Run without saving - result_no_save = run_from_json(self.params, save_results=False) - self.assertIsNone(result_no_save) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - # Verify interactive_spatial_plot was called correctly - mock_spatial_plot.assert_called() - call_args = mock_spatial_plot.call_args[1] - self.assertEqual(call_args['annotations'], ["renamed_phenotypes"]) - self.assertIsNone(call_args['feature']) - self.assertEqual(call_args['dot_size'], 3) - self.assertEqual(call_args['reverse_y_axis'], False) - - def test_error_validation(self) -> None: - """Test exact error messages for invalid parameters.""" - # Test missing annotation when Color_By is "Annotation" - params_bad = self.params.copy() - params_bad["Annotation_s_to_Highlight"] = [] - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - expected_msg = ( - 'Please set at least one value in the "Annotation(s) to ' - 'Highlight" parameter' + def test_interactive_spatial_plot_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run interactive spatial plot and verify outputs. + + Validates: + 1. saved_files dict has 'html' key + 2. HTML directory contains non-empty file(s) + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) - self.assertEqual(str(context.exception), expected_msg) - - # Test missing feature when Color_By is "Feature" - params_bad2 = self.params.copy() - params_bad2["Color_By"] = "Feature" - params_bad2["Feature_to_Highlight"] = "None" - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad2) - - expected_msg = 'Please set the "Feature to Highlight" parameter.' - self.assertEqual(str(context.exception), expected_msg) - - @patch('spac.templates.interactive_spatial_plot_template.' - 'interactive_spatial_plot') - def test_function_calls(self, mock_spatial_plot) -> None: - """Test that main function is called with correct parameters.""" - # Mock multiple plots with stratification - mock_fig1 = MagicMock() - mock_fig2 = MagicMock() - - mock_spatial_plot.return_value = [ - {'image_name': 'S1', 'image_object': mock_fig1}, - {'image_name': 'S2', 'image_object': mock_fig2} - ] - - # Test with stratification - params_strat = self.params.copy() - params_strat["Stratify_By"] = "sample" - params_strat["Color_By"] = "Feature" - params_strat["Feature_to_Highlight"] = "Gene1" - params_strat["Annotation_s_to_Highlight"] = [""] - - with patch('plotly.io.to_html', return_value="Mock"): - run_from_json(params_strat) - - # Verify function was called correctly - mock_spatial_plot.assert_called_once() - call_args = mock_spatial_plot.call_args[1] - - # Check parameter conversions - self.assertIsNone(call_args['annotations']) - self.assertEqual(call_args['feature'], "Gene1") - self.assertEqual(call_args['stratify_by'], "sample") - self.assertEqual(call_args['defined_color_map'], "_spac_colors") + + self.assertIsInstance(saved_files, dict) + self.assertIn("html", saved_files) + + html_paths = saved_files["html"] + self.assertGreaterEqual(len(html_paths), 1) + for html_path in html_paths: + html_file = Path(html_path) + self.assertTrue(html_file.exists()) + self.assertGreater(html_file.stat().st_size, 0) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_load_csv_files_with_config.py b/tests/templates/test_load_csv_files_with_config.py index 7f845708..38a8fa1e 100644 --- a/tests/templates/test_load_csv_files_with_config.py +++ b/tests/templates/test_load_csv_files_with_config.py @@ -1,5 +1,10 @@ -# tests/templates/test_load_csv_template.py -"""Unit tests for the Load CSV Files template.""" +# tests/templates/test_load_csv_files_with_config.py +""" +Real (non-mocked) unit test for the Load CSV Files template. + +Snowball test -- validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,7 +12,6 @@ import tempfile import unittest from pathlib import Path -from unittest.mock import patch, MagicMock import pandas as pd @@ -15,261 +19,86 @@ os.path.dirname(os.path.realpath(__file__)) + "/../../src" ) -from spac.templates.load_csv_files_with_config import run_from_json - - -def create_mock_csv_files(tmp_dir: Path) -> tuple: - """Create minimal CSV files for testing.""" - # Create first CSV file - csv1_data = pd.DataFrame({ - 'CellID': [1, 2, 3], - 'X_centroid': [10.0, 20.0, 30.0], - 'Y_centroid': [15.0, 25.0, 35.0], - 'cell_type': ['TypeA', 'TypeB', 'TypeA'] - }) - csv1_path = tmp_dir / 'sample1.csv' - csv1_data.to_csv(csv1_path, index=False) - - # Create second CSV file - csv2_data = pd.DataFrame({ - 'CellID': [4, 5, 6], - 'X_centroid': [40.0, 50.0, 60.0], - 'Y_centroid': [45.0, 55.0, 65.0], - 'cell_type': ['TypeB', 'TypeB', 'TypeA'] - }) - csv2_path = tmp_dir / 'sample2.csv' - csv2_data.to_csv(csv2_path, index=False) - - # Create configuration file - config_data = pd.DataFrame({ - 'file_name': ['sample1.csv', 'sample2.csv'], - 'slide_number': ['S1', 'S2'] - }) - config_path = tmp_dir / 'config.csv' - config_data.to_csv(config_path, index=False) - - return csv1_path, csv2_path, config_path +from spac.templates.load_csv_files_template import run_from_json class TestLoadCSVFilesWithConfig(unittest.TestCase): - """Unit tests for the Load CSV Files template.""" + """Real (non-mocked) tests for the load CSV files template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.tmp_path = Path(self.tmp_dir.name) - - # Create mock CSV files - self.csv1, self.csv2, self.config = create_mock_csv_files( - self.tmp_path - ) - - # Minimal parameters - self.params = { - "CSV_Files": str(self.tmp_path), - "CSV_Files_Configuration": str(self.config), - "String_Columns": [""], - "Output_File": "combined.csv" - } - - def tearDown(self) -> None: - self.tmp_dir.cleanup() - - def test_complete_io_workflow(self) -> None: - """Single I/O test covering all input/output scenarios.""" - # Test 1: Run with save_results=True - saved_files = run_from_json(self.params) - self.assertIn("combined.csv", saved_files) - output_path = Path(saved_files["combined.csv"]) - self.assertTrue(output_path.exists()) - - # Verify content - result_df = pd.read_csv(output_path) - self.assertEqual(len(result_df), 6) # 3 + 3 rows - self.assertIn('file_name', result_df.columns) - self.assertIn('slide_number', result_df.columns) - self.assertIn('CellID', result_df.columns) - - # Test 2: Run with save_results=False - df_result = run_from_json(self.params, save_results=False) - self.assertIsInstance(df_result, pd.DataFrame) - self.assertEqual(len(df_result), 6) - - # Test 3: JSON file input - json_path = self.tmp_path / "params.json" - with open(json_path, "w") as f: - json.dump(self.params, f) - saved_from_json = run_from_json(json_path) - self.assertIn("combined.csv", saved_from_json) - - # Test 4: String columns specified - params_with_strings = self.params.copy() - params_with_strings["String_Columns"] = ["CellID"] - df_with_strings = run_from_json(params_with_strings, save_results=False) - self.assertEqual(df_with_strings['CellID'].dtype, 'object') - - # Test 5: Special characters in column names - special_csv = pd.DataFrame({ - 'Cell-ID': [1, 2], - 'Area µm²': [100.0, 200.0], - 'CD4+': ['pos', 'neg'] - }) - special_path = self.tmp_path / 'special.csv' - special_csv.to_csv(special_path, index=False) - - special_config = pd.DataFrame({ - 'file_name': ['special.csv'], - 'experiment': ['Exp1'] - }) - special_config_path = self.tmp_path / 'special_config.csv' - special_config.to_csv(special_config_path, index=False) - params_special = { - "CSV_Files": str(self.tmp_path), - "CSV_Files_Configuration": str(special_config_path), - "String_Columns": [""] - } - df_special = run_from_json(params_special, save_results=False) - # Check column names were cleaned - self.assertIn('Cell_ID', df_special.columns) - self.assertIn('Area_um2', df_special.columns) - self.assertIn('CD4_pos', df_special.columns) + # Create CSV data directory + csv_dir = os.path.join(self.tmp_dir.name, "csv_data") + os.makedirs(csv_dir) - def test_error_messages(self) -> None: - """Test exact error messages for various failure scenarios.""" - # Test 1: Missing CSV file - bad_config = pd.DataFrame({ - 'file_name': ['missing.csv'], - 'slide_number': ['S1'] + df1 = pd.DataFrame({ + "Feature_A": [1.0, 2.0], + "Feature_B": [3.0, 4.0], + "ID": ["cell_1", "cell_2"], }) - bad_config_path = self.tmp_path / 'bad_config.csv' - bad_config.to_csv(bad_config_path, index=False) - - params_missing = self.params.copy() - params_missing["CSV_Files_Configuration"] = str(bad_config_path) - - with self.assertRaises(TypeError) as context: - run_from_json(params_missing) - expected_msg = "The following files are not found: missing.csv" - self.assertEqual(expected_msg, str(context.exception)) - - # Test 2: Empty CSV file - empty_path = self.tmp_path / 'empty.csv' - empty_path.write_text('') - - empty_config = pd.DataFrame({ - 'file_name': ['empty.csv'], - 'slide_number': ['S1'] + df2 = pd.DataFrame({ + "Feature_A": [5.0, 6.0], + "Feature_B": [7.0, 8.0], + "ID": ["cell_3", "cell_4"], }) - empty_config_path = self.tmp_path / 'empty_config.csv' - empty_config.to_csv(empty_config_path, index=False) - - params_empty = self.params.copy() - params_empty["CSV_Files_Configuration"] = str(empty_config_path) - with self.assertRaises(TypeError) as context: - run_from_json(params_empty) - expected_msg = 'The file: "empty.csv" is empty.' - self.assertEqual(expected_msg, str(context.exception)) + df1.to_csv(os.path.join(csv_dir, "data1.csv"), index=False) + df2.to_csv(os.path.join(csv_dir, "data2.csv"), index=False) - # Test 3: Invalid CSV file - invalid_path = self.tmp_path / 'invalid.csv' - # Create a truly invalid CSV that will cause parser error - invalid_path.write_text('col1,col2,col3\n"unclosed quote,value2,value3\nvalue4,value5') - - invalid_config = pd.DataFrame({ - 'file_name': ['invalid.csv'], - 'slide_number': ['S1'] + # Configuration CSV with file_name column + metadata + config_df = pd.DataFrame({ + "file_name": ["data1.csv", "data2.csv"], + "experiment": ["Exp1", "Exp2"], }) - invalid_config_path = self.tmp_path / 'invalid_config.csv' - invalid_config.to_csv(invalid_config_path, index=False) + config_file = os.path.join(self.tmp_dir.name, "config.csv") + config_df.to_csv(config_file, index=False) + + params = { + "CSV_Files": csv_dir, + "CSV_Files_Configuration": config_file, + "String_Columns": ["ID"], + "Output_Directory": self.tmp_dir.name, + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"}, + }, + } - params_invalid = self.params.copy() - params_invalid["CSV_Files_Configuration"] = str(invalid_config_path) + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) - with self.assertRaises(TypeError) as context: - run_from_json(params_invalid) - expected_msg = ( - 'The file "invalid.csv" could not be parsed. ' - 'Please check that the file is a valid CSV.' - ) - self.assertEqual(expected_msg, str(context.exception)) - - # Test 4: Invalid string_columns parameter - params_bad_strings = self.params.copy() - params_bad_strings["String_Columns"] = "not_a_list" + def tearDown(self) -> None: + self.tmp_dir.cleanup() - with self.assertRaises(ValueError) as context: - run_from_json(params_bad_strings) - expected_msg = ( - "String Columns must be a *list* of column names (strings)." + def test_load_csv_files_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: load CSV files with config and verify. + + Validates: + 1. saved_files dict has 'dataframe' key + 2. CSV exists and is non-empty + 3. Combined data has rows from both input files + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) - self.assertEqual(expected_msg, str(context.exception)) - - def test_metadata_mapping(self) -> None: - """Test that metadata columns are correctly mapped.""" - df_result = run_from_json(self.params, save_results=False) - - # Check slide_number mapping - sample1_rows = df_result[df_result['file_name'] == 'sample1.csv'] - sample2_rows = df_result[df_result['file_name'] == 'sample2.csv'] - - self.assertTrue(all(sample1_rows['slide_number'] == 'S1')) - self.assertTrue(all(sample2_rows['slide_number'] == 'S2')) - - @patch('builtins.print') - def test_console_output(self, mock_print) -> None: - """Test that progress is printed to console.""" - run_from_json(self.params, save_results=False) - - # Check for expected print statements - print_calls = [str(call[0][0]) for call in mock_print.call_args_list - if call[0]] - - # Should print processing messages - processing_msgs = [msg for msg in print_calls - if 'Processing file:' in msg] - self.assertEqual(len(processing_msgs), 2) # Two files - # Should print completion message - completion_msgs = [msg for msg in print_calls - if 'Load CSV Files completed' in msg] - self.assertTrue(len(completion_msgs) > 0) - - def test_duplicate_file_handling(self) -> None: - """Test handling of duplicate file names in config.""" - # Create config with duplicate entries - dup_config = pd.DataFrame({ - 'file_name': ['sample1.csv', 'sample1.csv'], - 'slide_number': ['S1', 'S2'] - }) - dup_config_path = self.tmp_path / 'dup_config.csv' - dup_config.to_csv(dup_config_path, index=False) - - params_dup = self.params.copy() - params_dup["CSV_Files_Configuration"] = str(dup_config_path) - - with self.assertRaises(RuntimeError) as context: - run_from_json(params_dup) - self.assertIn( - "Failed to process CSV files", - str(context.exception) - ) + self.assertIsInstance(saved_files, dict) + self.assertIn("dataframe", saved_files) - def test_string_columns_validation(self) -> None: - """Test validation of string_columns parameter.""" - # Test non-existent column - params_bad_col = self.params.copy() - params_bad_col["String_Columns"] = ["NonExistentColumn"] + csv_path = Path(saved_files["dataframe"]) + self.assertTrue(csv_path.exists()) + self.assertGreater(csv_path.stat().st_size, 0) - with self.assertRaises(ValueError): - run_from_json(params_bad_col) + result_df = pd.read_csv(csv_path) + self.assertEqual(len(result_df), 4) - # Test None handling - params_none = self.params.copy() - params_none["String_Columns"] = ["None"] - df_none = run_from_json(params_none, save_results=False) - self.assertIsInstance(df_none, pd.DataFrame) + mem_df = run_from_json(self.json_file, save_to_disk=False) + self.assertIsInstance(mem_df, pd.DataFrame) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_manual_phenotyping_template.py b/tests/templates/test_manual_phenotyping_template.py index 13d5ad82..c3a9227b 100644 --- a/tests/templates/test_manual_phenotyping_template.py +++ b/tests/templates/test_manual_phenotyping_template.py @@ -1,5 +1,15 @@ #!/usr/bin/env python3 -"""Unit tests for the Manual Phenotyping template.""" +# tests/templates/test_manual_phenotyping_template.py +""" +Real (non-mocked) unit test for the Manual Phenotyping template. + +Validates template I/O behaviour only: + - Expected output files are produced on disk + - Filenames follow the convention + - Output artifacts are non-empty + +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,10 +17,8 @@ import tempfile import unittest from pathlib import Path -from unittest.mock import patch, MagicMock import pandas as pd -import numpy as np sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -19,258 +27,106 @@ from spac.templates.manual_phenotyping_template import run_from_json -def create_mock_data_and_phenotypes(tmp_dir: Path) -> tuple: - """Create minimal data and phenotypes files for testing.""" - # Create mock expression data - n_cells = 20 # Simple scenario with 20 cells for testing - data = pd.DataFrame({ - 'cell_id': [f'cell_{i}' for i in range(n_cells)], - 'CD3D_expression': np.random.choice([0, 1], n_cells), - 'CD4_expression': np.random.choice([0, 1], n_cells), - 'CD8A_expression': np.random.choice([0, 1], n_cells), - 'FOXP3_expression': np.random.choice([0, 1], n_cells), - 'CD68_expression': np.random.choice([0, 1], n_cells), - 'CD20_expression': np.random.choice([0, 1], n_cells), - 'CD21_expression': np.random.choice([0, 1], n_cells), - 'CD56_expression': np.random.choice([0, 1], n_cells), +def _make_tiny_dataframe() -> pd.DataFrame: + """ + Minimal synthetic DataFrame with binary phenotype marker columns. + + 4 rows -- each row has one positive marker matching a phenotype rule. + """ + return pd.DataFrame({ + "cd4": [1, 0, 0, 1], + "cd8": [0, 1, 0, 0], + "cd20": [0, 0, 1, 0], + "marker_intensity": [1.5, 2.5, 3.5, 4.5], }) - data_path = tmp_dir / 'input_data.csv' - data.to_csv(data_path, index=False) - - # Create phenotypes definition - phenotypes = pd.DataFrame({ - 'phenotype_code': [ - 'CD3D+CD4+FOXP3+', - 'CD3D+CD4+', - 'CD3D+CD8A+', - 'CD68+', - 'CD20+' - ], - 'phenotype_name': [ - 'Regulatory T Cell', - 'Helper T Cell', - 'Cytotoxic T Cell', - 'Macrophage', - 'B Cell' - ] + + +def _make_phenotype_rules() -> pd.DataFrame: + """ + Phenotype rule table: maps binary codes to phenotype names. + + Each row uses a '+' or '-' code referencing column names. + """ + return pd.DataFrame({ + "phenotype_name": ["T_helper", "Cytotoxic_T", "B_cell"], + "phenotype_code": ["cd4+cd8-", "cd4-cd8+", "cd20+"], }) - phenotypes_path = tmp_dir / 'phenotypes.csv' - phenotypes.to_csv(phenotypes_path, index=False) - - return data_path, phenotypes_path class TestManualPhenotypingTemplate(unittest.TestCase): - """Unit tests for the Manual Phenotyping template.""" + """Real (non-mocked) tests for the manual phenotyping template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.tmp_path = Path(self.tmp_dir.name) - - # Create mock files - self.data_path, self.phenotypes_path = \ - create_mock_data_and_phenotypes(self.tmp_path) - - # Minimal parameters - self.params = { - "Upstream_Dataset": str(self.data_path), - "Phenotypes_Code": str(self.phenotypes_path), + self.in_file = os.path.join(self.tmp_dir.name, "input.csv") + self.rules_file = os.path.join(self.tmp_dir.name, "phenotypes.csv") + + _make_tiny_dataframe().to_csv(self.in_file, index=False) + _make_phenotype_rules().to_csv(self.rules_file, index=False) + + params = { + "Upstream_Dataset": self.in_file, + "Phenotypes_Code": self.rules_file, "Classification_Column_Prefix": "", - "Classification_Column_Suffix": "_expression", + "Classification_Column_Suffix": "", "Allow_Multiple_Phenotypes": True, "Manual_Annotation_Name": "manual_phenotype", - "Output_File": "phenotyped_data.csv" + "Output_Directory": self.tmp_dir.name, + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - @patch('spac.templates.manual_phenotyping_template.' - 'assign_manual_phenotypes') - def test_complete_io_workflow(self, mock_assign) -> None: - """Single I/O test covering all input/output scenarios.""" - # Mock the assign_manual_phenotypes function - def mock_assign_func(df, pheno, **kwargs): - # Add phenotype column with the correct annotation name - annotation_name = kwargs.get('annotation', 'manual_phenotype') - df[annotation_name] = np.random.choice( - ['T Cell', 'B Cell', 'Macrophage', 'no_label'], - len(df) - ) - return {'status': 'success'} - - mock_assign.side_effect = mock_assign_func - - # Change to temp directory for output - original_cwd = os.getcwd() - os.chdir(self.tmp_path) - - try: - # Test 1: Run with save_results=True - saved_files = run_from_json(self.params) - self.assertIn("phenotyped_data.csv", saved_files) - output_path = Path(saved_files["phenotyped_data.csv"]) - self.assertTrue(output_path.exists()) - - # Verify content - result_df = pd.read_csv(output_path) - self.assertEqual(len(result_df), 20) # Same number of cells - self.assertIn('manual_phenotype', result_df.columns) - self.assertIn('cell_id', result_df.columns) - - # Test 2: Run with save_results=False (in-memory) - df_result = run_from_json(self.params, save_results=False) - self.assertIsInstance(df_result, pd.DataFrame) - self.assertEqual(len(df_result), 20) - self.assertIn('manual_phenotype', df_result.columns) - - # Test 3: JSON file input - json_path = self.tmp_path / "params.json" - with open(json_path, "w") as f: - json.dump(self.params, f) - saved_from_json = run_from_json(json_path) - self.assertIn("phenotyped_data.csv", saved_from_json) - - # Test 4: Direct DataFrame input (chained workflow) - input_df = pd.read_csv(self.data_path) - params_df = self.params.copy() - params_df["Upstream_Dataset"] = input_df # Pass DataFrame - - df_from_df = run_from_json(params_df, save_results=False) - self.assertIsInstance(df_from_df, pd.DataFrame) - self.assertEqual(len(df_from_df), 20) - self.assertIn('manual_phenotype', df_from_df.columns) - - # Test 5: Custom parameters - params_custom = self.params.copy() - params_custom["Allow_Multiple_Phenotypes"] = False - params_custom["Manual_Annotation_Name"] = "cell_type" - params_custom["Output_File"] = "custom_output.csv" - - saved_custom = run_from_json(params_custom) - self.assertIn("custom_output.csv", saved_custom) - - # Verify custom annotation name - custom_df = pd.read_csv(saved_custom["custom_output.csv"]) - self.assertIn('cell_type', custom_df.columns) - - # Verify mock was called with correct parameters - call_args = mock_assign.call_args_list[-1] # Last call - self.assertEqual(call_args[1]['multiple'], False) - self.assertEqual(call_args[1]['annotation'], 'cell_type') - - finally: - os.chdir(original_cwd) - - @patch('spac.templates.manual_phenotyping_template.' - 'assign_manual_phenotypes') - def test_error_validation(self, mock_assign) -> None: - """Test exact error messages for various failure scenarios.""" - # Test 1: Missing phenotypes file - params_missing = self.params.copy() - params_missing["Phenotypes_Code"] = str( - self.tmp_path / "missing.csv" - ) - - with self.assertRaises(FileNotFoundError) as context: - run_from_json(params_missing) - - # Test 2: Missing input data file - params_no_input = self.params.copy() - params_no_input["Upstream_Dataset"] = str( - self.tmp_path / "nonexistent.csv" + def test_manual_phenotyping_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run manual phenotyping template and verify + output artifacts. + + Validates: + 1. saved_files is a dict with 'dataframe' key + 2. Output CSV exists and is non-empty + 3. Phenotype annotation column is present in output + """ + # -- Act (save_to_disk=True) ----------------------------------- + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) - - with self.assertRaises(FileNotFoundError): - run_from_json(params_no_input) - - # Test 3: Invalid CSV file for phenotypes - # SPAC function error - # Mock to simulate SPAC function error - mock_assign.side_effect = ValueError("Invalid phenotype code format") - - with self.assertRaises(ValueError) as context: - run_from_json(self.params) - - expected_msg = "Invalid phenotype code format" - self.assertEqual(str(context.exception), expected_msg) - @patch('spac.templates.manual_phenotyping_template.' - 'assign_manual_phenotypes') - @patch('builtins.print') - def test_console_output(self, mock_print, mock_assign) -> None: - """Test that expected messages are printed to console.""" - # Mock the assign function - def mock_assign_func(df, pheno, **kwargs): - df['manual_phenotype'] = ['T Cell'] * len(df) - return {'status': 'success'} - - mock_assign.side_effect = mock_assign_func - - # Change to temp directory - original_cwd = os.getcwd() - os.chdir(self.tmp_path) - - try: - run_from_json(self.params) - - # Check for expected print statements - print_calls = [str(call[0][0]) for call in - mock_print.call_args_list if call[0]] - - # Should print phenotypes DataFrame - phenotypes_printed = any('phenotype_code' in str(call) - for call in print_calls) - self.assertTrue(phenotypes_printed) - - # Should print completion message - completion_msgs = [ - msg for msg in print_calls - if 'Manual Phenotyping completed successfully' in msg - ] - self.assertTrue(len(completion_msgs) > 0) - - # Should print file save message - save_msgs = [ - msg for msg in print_calls - if 'Manual Phenotyping completed →' in msg - ] - self.assertTrue(len(save_msgs) > 0) - - finally: - os.chdir(original_cwd) + # -- Assert: return type --------------------------------------- + self.assertIsInstance(saved_files, dict) + + # -- Assert: CSV file exists and is non-empty ------------------ + self.assertIn("dataframe", saved_files) + csv_path = Path(saved_files["dataframe"]) + self.assertTrue(csv_path.exists(), f"CSV not found: {csv_path}") + self.assertGreater(csv_path.stat().st_size, 0) + + # -- Assert: phenotype column present -------------------------- + result_df = pd.read_csv(csv_path) + self.assertIn("manual_phenotype", result_df.columns) + # At least some rows should have assigned phenotypes + non_null = result_df["manual_phenotype"].dropna() + self.assertGreater(len(non_null), 0) + + # -- Act (save_to_disk=False) ---------------------------------- + mem_df = run_from_json( + self.json_file, + save_to_disk=False, + ) - @patch('spac.templates.manual_phenotyping_template.' - 'assign_manual_phenotypes') - def test_phenotype_distribution_output(self, mock_assign) -> None: - """Test that phenotype distribution is correctly calculated/printed.""" - # Create specific phenotype assignments - def mock_assign_func(df, pheno, **kwargs): - # Assign specific phenotypes for testing - phenotypes = ['T Cell'] * 10 + ['B Cell'] * 5 + ['no_label'] * 5 - df[kwargs.get('annotation', 'manual_phenotype')] = phenotypes[:len(df)] - return {'status': 'success'} - - mock_assign.side_effect = mock_assign_func - - with patch('builtins.print') as mock_print: - df_result = run_from_json(self.params, save_results=False) - - # Check distribution in result - counts = df_result['manual_phenotype'].value_counts() - self.assertEqual(counts['T Cell'], 10) - self.assertEqual(counts['B Cell'], 5) - self.assertEqual(counts['no_label'], 5) - - # Check that distribution was printed - print_calls = [str(call[0][0]) for call in - mock_print.call_args_list if call[0]] - distribution_printed = any( - 'Phenotype distribution' in str(call) - for call in print_calls - ) - self.assertTrue(distribution_printed) + # -- Assert: in-memory return is DataFrame --------------------- + self.assertIsInstance(mem_df, pd.DataFrame) + self.assertIn("manual_phenotype", mem_df.columns) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_nearest_neighbor_calculation_template.py b/tests/templates/test_nearest_neighbor_calculation_template.py index c0e3f894..cc888a57 100644 --- a/tests/templates/test_nearest_neighbor_calculation_template.py +++ b/tests/templates/test_nearest_neighbor_calculation_template.py @@ -1,5 +1,10 @@ # tests/templates/test_nearest_neighbor_calculation_template.py -"""Unit tests for the Nearest Neighbor Calculation template.""" +""" +Real (non-mocked) unit test for the Nearest Neighbor Calculation template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,156 +12,88 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" ) -from spac.templates.nearest_neighbor_calculation_template import ( - run_from_json -) +from spac.templates.nearest_neighbor_calculation_template import run_from_json -def mock_adata(n_cells: int = 10) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 8 cells with spatial coords and annotation.""" + rng = np.random.default_rng(42) + X = rng.random((8, 2)) obs = pd.DataFrame({ - "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], - "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] + "cell_type": ["A", "B", "A", "B", "A", "B", "A", "B"], }) - x_mat = rng.normal(size=(n_cells, 3)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = ["Gene1", "Gene2", "Gene3"] - # Add spatial coordinates - adata.obsm["spatial"] = rng.random((n_cells, 2)) * 100 + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + spatial = rng.random((8, 2)) * 100 + adata = ad.AnnData(X=X, obs=obs, var=var) + adata.obsm["spatial"] = spatial return adata class TestNearestNeighborCalculationTemplate(unittest.TestCase): - """Unit tests for the Nearest Neighbor Calculation template.""" + """Real (non-mocked) tests for nearest neighbor calculation.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "output" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters - from NIDAP template - self.params = { + params = { "Upstream_Analysis": self.in_file, "Annotation": "cell_type", "ImageID": "None", - "Nearest_Neighbor_Associated_Table": "spatial_distance", - "Verbose": True, - "Output_File": self.out_file, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Mock the calculate_nearest_neighbor function - with patch( - 'spac.templates.nearest_neighbor_calculation_template.' - 'calculate_nearest_neighbor' - ) as mock_calc_nn: - # Mock function adds a distance matrix to adata.obsm - def side_effect(adata, annotation, spatial_associated_table, - imageid, label, verbose): - # Simulate adding distance matrix to obsm - n_cells = adata.n_obs - unique_types = adata.obs[annotation].unique() - n_types = len(unique_types) - # Create mock distance dataframe - dist_df = pd.DataFrame( - np.random.rand(n_cells, n_types), - columns=unique_types, - index=adata.obs.index - ) - adata.obsm[label] = dist_df - - mock_calc_nn.side_effect = side_effect - - # Test 1: Run with default parameters - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - # Verify file was saved - self.assertTrue(len(result) > 0) - - # Test 2: Run without saving - result_no_save = run_from_json( - self.params, save_results=False - ) - # Check appropriate return type - self.assertIsInstance(result_no_save, ad.AnnData) - # Verify nearest neighbor distances were added - self.assertIn("spatial_distance", result_no_save.obsm) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - def test_function_calls(self) -> None: - """Test that main function is called with correct parameters.""" - with patch( - 'spac.templates.nearest_neighbor_calculation_template.' - 'calculate_nearest_neighbor' - ) as mock_calc_nn: - # Test with different parameters - params_alt = self.params.copy() - params_alt["ImageID"] = "sample" - params_alt["Nearest_Neighbor_Associated_Table"] = "nn_distances" - params_alt["Verbose"] = False - - run_from_json(params_alt, save_results=False) - - # Verify function was called correctly - mock_calc_nn.assert_called_once() - call_args = mock_calc_nn.call_args - - # Check parameter conversions - self.assertEqual(call_args[1]['annotation'], "cell_type") - self.assertEqual( - call_args[1]['spatial_associated_table'], "spatial" - ) - self.assertEqual(call_args[1]['imageid'], "sample") - self.assertEqual(call_args[1]['label'], "nn_distances") - self.assertEqual(call_args[1]['verbose'], False) - - def test_imageid_none_conversion(self) -> None: - """Test that string 'None' is converted to actual None.""" - with patch( - 'spac.templates.nearest_neighbor_calculation_template.' - 'calculate_nearest_neighbor' - ) as mock_calc_nn: - # Test with "None" string - self.params["ImageID"] = "None" - run_from_json(self.params, save_results=False) - - # Verify imageid was converted to None - call_args = mock_calc_nn.call_args - self.assertIsNone(call_args[1]['imageid']) + def test_nearest_neighbor_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: calculate nearest neighbors and verify outputs. + + Validates: + 1. saved_files dict has 'analysis' key + 2. Pickle contains AnnData with nearest neighbor results + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, + ) + + self.assertIsInstance(saved_files, dict) + self.assertIn("analysis", saved_files) + + pkl_path = Path(saved_files["analysis"]) + self.assertTrue(pkl_path.exists()) + self.assertGreater(pkl_path.stat().st_size, 0) + + with open(pkl_path, "rb") as f: + result_adata = pickle.load(f) + self.assertIsInstance(result_adata, ad.AnnData) + + mem_adata = run_from_json(self.json_file, save_to_disk=False) + self.assertIsInstance(mem_adata, ad.AnnData) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_neighborhood_profile_template.py b/tests/templates/test_neighborhood_profile_template.py index e286771b..36a1fad9 100644 --- a/tests/templates/test_neighborhood_profile_template.py +++ b/tests/templates/test_neighborhood_profile_template.py @@ -1,5 +1,10 @@ # tests/templates/test_neighborhood_profile_template.py -"""Unit tests for the Neighborhood Profile template.""" +""" +Real (non-mocked) unit test for the Neighborhood Profile template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,13 +12,11 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -22,187 +25,73 @@ from spac.templates.neighborhood_profile_template import run_from_json -def mock_adata(n_cells: int = 20) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) - - # Create simple cell types that repeat properly for any n_cells - cell_types = ["T cells", "B cells", "Tumor cells"] +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 20 cells with spatial coords and annotation.""" + rng = np.random.default_rng(42) + X = rng.random((20, 2)) obs = pd.DataFrame({ - "cell_type": [cell_types[i % len(cell_types)] - for i in range(n_cells)], - "slide": (["Slide1", "Slide2"] * ((n_cells + 1) // 2))[:n_cells] + "cell_type": (["A"] * 10) + (["B"] * 10), }) - - x_mat = rng.normal(size=(n_cells, 3)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = ["Marker1", "Marker2", "Marker3"] - - # Add spatial coordinates - adata.obsm["spatial"] = rng.random((n_cells, 2)) * 100 - + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + spatial = rng.random((20, 2)) * 100 + adata = ad.AnnData(X=X, obs=obs, var=var) + adata.obsm["spatial"] = spatial return adata class TestNeighborhoodProfileTemplate(unittest.TestCase): - """Unit tests for the Neighborhood Profile template.""" + """Real (non-mocked) tests for the neighborhood profile template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters from NIDAP template - self.params = { + params = { "Upstream_Analysis": self.in_file, "Annotation_of_interest": "cell_type", - "Bins": [0, 10, 30, 50], - "Stratify_By": "slide", - "Anchor_Neighbor_List": [ - "T cells;B cells", - "Tumor cells;T cells" - ] + "Bins": [10, 25, 50], + "Anchor_Neighbor_List": ["A;B"], + "Stratify_By": "None", + "Output_Directory": self.tmp_dir.name, + "outputs": { + "dataframe": {"type": "directory", "name": "dataframe_dir"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Mock the neighborhood_profile function to add expected data - with patch('spac.templates.neighborhood_profile_template.' - 'neighborhood_profile') as mock_np: - # Setup mock to add the expected data structures - def mock_neighborhood_profile(adata, **kwargs): - # Add mock neighborhood profile data - n_cells = adata.n_obs - n_phenotypes = 3 # T cells, B cells, Tumor cells - n_bins = len(kwargs['distances']) - 1 # 3 bins - - # Create mock profile data - adata.obsm["neighborhood_profile"] = np.random.rand( - n_cells, n_phenotypes, n_bins - ) - - # Add labels to uns - adata.uns["neighborhood_profile"] = { - "labels": np.array([ - "T cells", "B cells", "Tumor cells" - ]) - } - - mock_np.side_effect = mock_neighborhood_profile - - # Test 1: Run with default parameters (save files) - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - - # Should have 2 CSV files based on anchor_neighbor_list - self.assertEqual(len(result), 2) - - # Check filenames - expected_files = [ - "anchor_T cells_neighbor_B cells.csv", - "anchor_Tumor cells_neighbor_T cells.csv" - ] - for expected in expected_files: - self.assertIn(expected, result) - - # Test 2: Run without saving - result_no_save = run_from_json( - self.params, save_results=False - ) - # Should return dict of dataframes - self.assertIsInstance(result_no_save, dict) - self.assertEqual(len(result_no_save), 2) - - # Check that keys are tuples - for key in result_no_save: - self.assertIsInstance(key, tuple) - self.assertEqual(len(key), 2) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - self.assertEqual(len(result_json), 2) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Mock neighborhood_profile to add required data structures - with patch('spac.templates.neighborhood_profile_template.' - 'neighborhood_profile') as mock_np: - def mock_neighborhood_profile(adata, **kwargs): - adata.obsm["neighborhood_profile"] = np.random.rand( - adata.n_obs, 2, 3 # Only 2 phenotypes - ) - adata.uns["neighborhood_profile"] = { - "labels": np.array([ - "T cells", "B cells" - ]) # Missing "Unknown" - } - - mock_np.side_effect = mock_neighborhood_profile - - # Test with invalid neighbor label - params_bad = self.params.copy() - params_bad["Anchor_Neighbor_List"] = ["T cells;Unknown"] - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - # Check exact error message - expected_msg = ("Neighbor label 'Unknown' not found in " - "neighborhood_profile labels.") - self.assertEqual(str(context.exception), expected_msg) - - @patch('spac.templates.neighborhood_profile_template.' - 'neighborhood_profile') - def test_function_calls(self, mock_np) -> None: - """Test that main function is called with correct parameters.""" - # Mock the neighborhood_profile function - def mock_neighborhood_profile(adata, **kwargs): - # Add required data structures - adata.obsm["neighborhood_profile"] = np.zeros( - (adata.n_obs, 3, 3) - ) - adata.uns["neighborhood_profile"] = { - "labels": np.array(["T cells", "B cells", "Tumor cells"]) - } - - mock_np.side_effect = mock_neighborhood_profile - - # Test with None slide_names - params_alt = self.params.copy() - params_alt["Stratify_By"] = "None" - - run_from_json(params_alt, save_results=False) - - # Verify function was called correctly - mock_np.assert_called_once() - call_args = mock_np.call_args - - # Check specific parameters - self.assertEqual(call_args[1]['phenotypes'], "cell_type") - self.assertEqual(call_args[1]['distances'], [0.0, 10.0, 30.0, 50.0]) - self.assertIsNone(call_args[1]['regions']) # "None" -> None - self.assertEqual(call_args[1]['spatial_key'], "spatial") - self.assertIsNone(call_args[1]['normalize']) - self.assertEqual( - call_args[1]['associated_table_name'], "neighborhood_profile" + def test_neighborhood_profile_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: compute neighborhood profiles and verify. + + Validates: + 1. saved_files dict has 'dataframe' key + 2. Output directory contains CSV file(s) + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) + self.assertIsInstance(saved_files, dict) + self.assertIn("dataframe", saved_files) + + csv_paths = saved_files["dataframe"] + self.assertGreaterEqual(len(csv_paths), 1) + for csv_path in csv_paths: + csv_file = Path(csv_path) + self.assertTrue(csv_file.exists()) + self.assertGreater(csv_file.stat().st_size, 0) + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_normalize_batch_template.py b/tests/templates/test_normalize_batch_template.py index 66ade9b7..f6d87e61 100644 --- a/tests/templates/test_normalize_batch_template.py +++ b/tests/templates/test_normalize_batch_template.py @@ -1,5 +1,10 @@ # tests/templates/test_normalize_batch_template.py -"""Unit tests for the Normalize Batch template.""" +""" +Real (non-mocked) unit test for the Normalize Batch template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,13 +12,11 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -22,139 +25,73 @@ from spac.templates.normalize_batch_template import run_from_json -def mock_adata(n_cells: int = 10) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 6 cells, 2 genes, 2 batches for batch normalization.""" + rng = np.random.default_rng(42) + X = rng.integers(1, 50, size=(6, 2)).astype(float) obs = pd.DataFrame({ - "batch": (["Batch1", "Batch2"] * ((n_cells + 1) // 2))[:n_cells], - "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] + "batch": ["A", "A", "A", "B", "B", "B"], + "cell_type": ["T", "B", "T", "B", "T", "B"], }) - x_mat = rng.normal(size=(n_cells, 3)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = ["Gene1", "Gene2", "Gene3"] - # Add an empty layers dict - adata.layers = {} - return adata + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + return ad.AnnData(X=X, obs=obs, var=var) class TestNormalizeBatchTemplate(unittest.TestCase): - """Unit tests for the Normalize Batch template.""" + """Real (non-mocked) tests for the normalize batch template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "output" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters from JSON template - self.params = { + params = { "Upstream_Analysis": self.in_file, - "Need_Normalization": False, - "Input_Table_Name": "Original", - "Output_Table_Name": "batch_normalized_table", "Annotation": "batch", - "Normalization_Method": "median", - "Take_Log": False, - "Output_File": self.out_file, + "Need_Normalization": True, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - @patch('spac.templates.normalize_batch_template.batch_normalize') - def test_complete_io_workflow(self, mock_batch_normalize) -> None: - """Single I/O test covering input/output scenarios.""" - # Mock the batch_normalize function - def mock_normalize(adata, **kwargs): - # Simulate normalization by adding a layer - adata.layers[kwargs['output_layer']] = adata.X.copy() - return adata - - mock_batch_normalize.side_effect = mock_normalize - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with Need_Normalization=False (no normalization) - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - self.assertTrue(len(result) > 0) - # Verify batch_normalize was NOT called - mock_batch_normalize.assert_not_called() - - # Test 2: Run with Need_Normalization=True - params_norm = self.params.copy() - params_norm["Need_Normalization"] = True - - result_norm = run_from_json(params_norm) - self.assertIsInstance(result_norm, dict) - # Verify batch_normalize WAS called - mock_batch_normalize.assert_called_once() - - # Test 3: Run without saving - result_no_save = run_from_json(params_norm, save_results=False) - self.assertIsInstance(result_no_save, ad.AnnData) - - # Test 4: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - @patch('spac.templates.normalize_batch_template.batch_normalize') - def test_function_calls(self, mock_batch_normalize) -> None: - """Test that main function is called with correct parameters.""" - # Mock the batch_normalize function to modify adata in-place - def mock_normalize(adata, **kwargs): - # Simulate normalization by adding a layer - output_layer = kwargs.get('output_layer', 'batch_normalized_table') - adata.layers[output_layer] = adata.X.copy() - # batch_normalize modifies in-place, returns None - return None - - mock_batch_normalize.side_effect = mock_normalize - - # Test with normalization enabled - params_enabled = self.params.copy() - params_enabled["Need_Normalization"] = True - params_enabled["Normalization_Method"] = "z-score" - params_enabled["Take_Log"] = True - - run_from_json(params_enabled, save_results=False) - - # Verify function was called correctly - mock_batch_normalize.assert_called_once() - call_args = mock_batch_normalize.call_args - - # Check specific parameter conversions - self.assertEqual(call_args[1]['annotation'], "batch") - # "Original" → None - self.assertEqual(call_args[1]['input_layer'], None) - self.assertEqual( - call_args[1]['output_layer'], "batch_normalized_table" + def test_normalize_batch_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run normalize batch and verify outputs. + + Validates: + 1. saved_files is a dict with 'analysis' key + 2. Output pickle exists, is non-empty, and contains AnnData + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) - self.assertEqual(call_args[1]['method'], "z-score") - self.assertEqual(call_args[1]['log'], True) - def test_parameter_defaults(self) -> None: - """Test that default parameters work correctly.""" - # Minimal required parameters only - params_minimal = { - "Upstream_Analysis": self.in_file, - "Annotation": "batch" - } - - with patch('spac.templates.normalize_batch_template.batch_normalize'): - result = run_from_json(params_minimal) - self.assertIsInstance(result, dict) + self.assertIsInstance(saved_files, dict) + self.assertIn("analysis", saved_files) + + pkl_path = Path(saved_files["analysis"]) + self.assertTrue(pkl_path.exists()) + self.assertGreater(pkl_path.stat().st_size, 0) + + with open(pkl_path, "rb") as f: + result_adata = pickle.load(f) + self.assertIsInstance(result_adata, ad.AnnData) + + mem_adata = run_from_json(self.json_file, save_to_disk=False) + self.assertIsInstance(mem_adata, ad.AnnData) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_phenograph_clustering_template.py b/tests/templates/test_phenograph_clustering_template.py index 14e485b5..d87dc3fe 100644 --- a/tests/templates/test_phenograph_clustering_template.py +++ b/tests/templates/test_phenograph_clustering_template.py @@ -1,5 +1,10 @@ # tests/templates/test_phenograph_clustering_template.py -"""Unit tests for the Phenograph Clustering template.""" +""" +Real (non-mocked) unit test for the Phenograph Clustering template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,13 +12,11 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -22,128 +25,79 @@ from spac.templates.phenograph_clustering_template import run_from_json -def mock_adata(n_cells: int = 100) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) - obs = pd.DataFrame({ - "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], - "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] - }) - # Create expression matrix with some structure for clustering - x_mat = rng.normal(size=(n_cells, 10)) - # Add some signal to make clustering meaningful - # First half of cells higher in first 5 genes - x_mat[:n_cells//2, :5] += 2.0 - # Second half higher in last 5 genes - x_mat[n_cells//2:, 5:] += 2.0 - - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = [f"Gene{i}" for i in range(10)] - adata.var.index.name = None # Match NIDAP style - return adata +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 50 cells, 5 genes for Phenograph clustering.""" + rng = np.random.default_rng(42) + # Two distinct clusters + X_a = rng.normal(0, 1, size=(25, 5)) + X_b = rng.normal(5, 1, size=(25, 5)) + X = np.vstack([X_a, X_b]) + obs = pd.DataFrame({"cell_type": ["A"] * 25 + ["B"] * 25}) + var = pd.DataFrame(index=[f"Gene_{i}" for i in range(5)]) + return ad.AnnData(X=X, obs=obs, var=var) class TestPhenographClusteringTemplate(unittest.TestCase): - """Unit tests for the Phenograph Clustering template.""" + """Real (non-mocked) tests for the phenograph clustering template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "output" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters - using proper Python types for defaults - self.params = { + params = { "Upstream_Analysis": self.in_file, "Table_to_Process": "Original", - "K_Nearest_Neighbors": 30, + "K_Nearest_Neighbors": 10, "Seed": 42, "Resolution_Parameter": 1.0, "Output_Annotation_Name": "phenograph", - "Resolution_List": [], - "Number_of_Iterations": 100, - "Output_File": self.out_file, + "Number_of_Iterations": 10, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - # Verify file was saved - self.assertTrue(len(result) > 0) - output_path = list(result.values())[0] - self.assertTrue(os.path.exists(output_path)) - - # Load and verify the output - with open(output_path, 'rb') as f: - adata_out = pickle.load(f) - self.assertIn("phenograph", adata_out.obs.columns) - - # Test 2: Run without saving - result_no_save = run_from_json(self.params, save_results=False) - # Check appropriate return type based on template - self.assertIsInstance(result_no_save, ad.AnnData) - self.assertIn("phenograph", result_no_save.obs.columns) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - def test_function_calls(self) -> None: - """Test that main function is called with correct parameters.""" - with patch('spac.templates.phenograph_clustering_template.' - 'phenograph_clustering') as mock_pheno: - # Mock the main function - def mock_clustering(adata, **kwargs): - # Add the phenograph column - n_half = len(adata) // 2 - adata.obs['phenograph'] = pd.Categorical( - ['0'] * n_half + ['1'] * (len(adata) - n_half) - ) - return None - - mock_pheno.side_effect = mock_clustering - - # Test with custom annotation name - params_custom = self.params.copy() - params_custom["Output_Annotation_Name"] = "my_clusters" - params_custom["K_Nearest_Neighbors"] = 50 - params_custom["Resolution_Parameter"] = 0.5 - - result = run_from_json(params_custom, save_results=False) - - # Verify function was called correctly - mock_pheno.assert_called_once() - call_args = mock_pheno.call_args - - # Check specific parameter conversions - self.assertEqual(call_args[1]['k'], 50) - self.assertEqual(call_args[1]['resolution_parameter'], 0.5) - self.assertEqual(call_args[1]['seed'], 42) - self.assertEqual(call_args[1]['n_iterations'], 100) - # "Original" -> None - self.assertEqual(call_args[1]['layer'], None) - - # Check that phenograph was renamed - self.assertIn("my_clusters", result.obs.columns) - self.assertNotIn("phenograph", result.obs.columns) + def test_phenograph_clustering_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run phenograph clustering and verify outputs. + + Validates: + 1. saved_files dict has 'analysis' key + 2. Pickle contains AnnData with 'phenograph' obs column + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, + ) + + self.assertIsInstance(saved_files, dict) + self.assertIn("analysis", saved_files) + + pkl_path = Path(saved_files["analysis"]) + self.assertTrue(pkl_path.exists()) + self.assertGreater(pkl_path.stat().st_size, 0) + + with open(pkl_path, "rb") as f: + result_adata = pickle.load(f) + self.assertIsInstance(result_adata, ad.AnnData) + self.assertIn("phenograph", result_adata.obs.columns) + + mem_adata = run_from_json(self.json_file, save_to_disk=False) + self.assertIsInstance(mem_adata, ad.AnnData) + self.assertIn("phenograph", mem_adata.obs.columns) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_posit_it_python_template.py b/tests/templates/test_posit_it_python_template.py index 926d57f1..fdfd64f6 100644 --- a/tests/templates/test_posit_it_python_template.py +++ b/tests/templates/test_posit_it_python_template.py @@ -1,18 +1,20 @@ # tests/templates/test_posit_it_python_template.py -"""Unit tests for the Posit-It-Python template.""" +""" +Real (non-mocked) unit test for the Posit-It Python template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os import sys import tempfile import unittest -import warnings from pathlib import Path -from unittest.mock import patch, MagicMock import matplotlib -matplotlib.use("Agg") # Headless backend for CI -import matplotlib.pyplot as plt +matplotlib.use("Agg") sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -22,109 +24,108 @@ class TestPostItPythonTemplate(unittest.TestCase): - """Unit tests for the Posit-It-Python template.""" + """Real (non-mocked) tests for the posit-it python template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.out_file = "graphicsFile.png" - # Minimal parameters from NIDAP template - self.params = { - "Label": "Post-It", - "Label_font_size": "80", - "Label_font_type": "normal", - "Label_Bold": "False", + params = { + "Label": "Test Note", "Label_font_color": "Black", + "Label_font_size": "40", + "Label_font_type": "normal", "Label_font_family": "Arial", + "Label_Bold": "False", "Background_fill_color": "Yellow1", "Background_fill_opacity": "10", - "Page_width": "18", - "Page_height": "6", - "Page_DPI": "300", - "Output_File": self.out_file, + "Page_width": "6", + "Page_height": "2", + "Page_DPI": "72", + "Output_Directory": self.tmp_dir.name, + "outputs": { + "figures": {"type": "directory", "name": "figures_dir"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - # Clean up any matplotlib figures - plt.close('all') - - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - self.assertIn(self.out_file, result) - # Check file was created - self.assertTrue(os.path.exists(result[self.out_file])) - # Clean up - os.remove(result[self.out_file]) - - # Test 2: Run without saving - result_no_save = run_from_json(self.params, save_results=False) - # Check appropriate return type - should be a figure - self.assertIsInstance(result_no_save, plt.Figure) - plt.close(result_no_save) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - # Clean up - if os.path.exists(result_json[self.out_file]): - os.remove(result_json[self.out_file]) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test invalid integer conversion for font size - params_bad = self.params.copy() - params_bad["Label_font_size"] = "invalid_number" - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - # Check exact error message - expected_msg = ( - "Error: can't convert Label_font_size to integer. " - "Received:\"invalid_number\"" + + def test_posit_it_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run posit-it template and verify outputs. + + Validates: + 1. save_to_disk=True returns a dict with 'figures' key + 2. Figures directory contains a non-empty PNG + 3. save_to_disk=False returns a matplotlib Figure with correct text + """ + # -- Act (save_to_disk=True): write outputs to disk ------------ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + show_plot=False, + output_dir=self.tmp_dir.name, + ) + + # -- Act (save_to_disk=False): get figure in memory ------------ + fig = run_from_json( + self.json_file, + save_to_disk=False, + show_plot=False, + ) + + # -- Assert: return type --------------------------------------- + self.assertIsInstance( + saved_files, dict, + f"Expected dict from run_from_json, got {type(saved_files)}" + ) + + # -- Assert: figures directory contains at least one PNG ------- + self.assertIn("figures", saved_files, + "Missing 'figures' key in saved_files") + figure_paths = saved_files["figures"] + self.assertGreaterEqual( + len(figure_paths), 1, "No figure files were saved" + ) + + for fig_path in figure_paths: + fig_file = Path(fig_path) + self.assertTrue( + fig_file.exists(), f"Figure not found: {fig_path}" + ) + self.assertGreater( + fig_file.stat().st_size, 0, + f"Figure file is empty: {fig_path}" + ) + self.assertEqual( + fig_file.suffix, ".png", + f"Expected .png extension, got {fig_file.suffix}" + ) + + # -- Assert: in-memory figure is valid ------------------------- + import matplotlib.figure + self.assertIsInstance( + fig, matplotlib.figure.Figure, + f"Expected matplotlib Figure, got {type(fig)}" + ) + + # The figure text at (0.5, 0.5) should contain "Test Note" + text_artists = fig.texts + self.assertGreaterEqual( + len(text_artists), 1, + "Figure has no text artists" + ) + # First text artist is the label placed by fig.text(0.5, 0.5, ...) + self.assertEqual( + text_artists[0].get_text(), "Test Note", + f"Expected figure text 'Test Note', " + f"got '{text_artists[0].get_text()}'" ) - self.assertEqual(str(context.exception), expected_msg) - - def test_function_calls(self) -> None: - """Test that function is called with correct parameters.""" - # Test with custom text and colors - params_custom = self.params.copy() - params_custom["Label"] = "Test Label" - params_custom["Label_font_color"] = "Red1" - params_custom["Background_fill_color"] = "Blue1" - params_custom["Label_Bold"] = "True" - - result = run_from_json(params_custom, save_results=False) - - # Verify figure was created with correct properties - self.assertIsInstance(result, plt.Figure) - # Check figure size - self.assertEqual(result.get_figwidth(), 18.0) - self.assertEqual(result.get_figheight(), 6.0) - - plt.close(result) - - def test_minimal_params(self) -> None: - """Test with minimal parameters using defaults.""" - minimal_params = {} # All defaults from JSON - - result = run_from_json(minimal_params, save_results=False) - - # Should still create a valid figure - self.assertIsInstance(result, plt.Figure) - plt.close(result) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_quantile_scaling_template.py b/tests/templates/test_quantile_scaling_template.py index 814bcc65..13c93046 100644 --- a/tests/templates/test_quantile_scaling_template.py +++ b/tests/templates/test_quantile_scaling_template.py @@ -1,5 +1,10 @@ # tests/templates/test_quantile_scaling_template.py -"""Unit tests for the Quantile Scaling template.""" +""" +Real (non-mocked) unit test for the Quantile Scaling template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,13 +12,11 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -22,189 +25,76 @@ from spac.templates.quantile_scaling_template import run_from_json -def mock_adata(n_cells: int = 10) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) - # Create expression data with variance for normalization - x_mat = rng.exponential(scale=5, size=(n_cells, 5)) - obs = pd.DataFrame({ - "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], - "batch": (["Batch1", "Batch2"] * ((n_cells + 1) // 2))[:n_cells] - }) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = [f"Marker{i}" for i in range(5)] - # Add an existing layer for testing - adata.layers["arcsinh"] = np.arcsinh(x_mat / 5) - return adata +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 4 cells, 2 genes for quantile scaling.""" + rng = np.random.default_rng(42) + X = rng.integers(1, 100, size=(4, 2)).astype(float) + obs = pd.DataFrame({"cell_type": ["A", "B", "A", "B"]}) + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + return ad.AnnData(X=X, obs=obs, var=var) class TestQuantileScalingTemplate(unittest.TestCase): - """Unit tests for the Quantile Scaling template.""" + """Real (non-mocked) tests for the quantile scaling template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "output" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters from NIDAP template - self.params = { + params = { "Upstream_Analysis": self.in_file, - "Table_to_Process": "arcsinh", - "Low_Quantile": "0.02", - "High_Quantile": "0.98", - "Interpolation": "nearest", - "Output_Table_Name": "scaled_arcsinh", - "Per_Batch": "False", - "Annotation": "", - "Output_File": self.out_file, + "Table_to_Normalize": "Original", + "Lower_Quantile": "0.01", + "Upper_Quantile": "0.99", + "Output_Directory": self.tmp_dir.name, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"}, + "html": {"type": "directory", "name": "html_dir"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - @patch('spac.templates.quantile_scaling_template.normalize_features') - def test_complete_io_workflow(self, mock_normalize) -> None: - """Single I/O test covering input/output scenarios.""" - # Mock the normalize_features function - def mock_transform(adata, **kwargs): - # Simulate normalization by adding a layer - output_layer = kwargs['output_layer'] - input_layer = kwargs.get('input_layer') - data = adata.layers[input_layer] if input_layer else adata.X - # Simple quantile scaling simulation - adata.layers[output_layer] = ( - (data - np.min(data)) / (np.max(data) - np.min(data)) - ) - return adata - - mock_normalize.side_effect = mock_transform - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - # Verify file was saved - self.assertTrue(len(result) > 0) - pickle_files = [f for f in result.values() if '.pickle' in str(f)] - self.assertTrue(len(pickle_files) > 0) - - # Test 2: Run without saving - result_no_save = run_from_json( - self.params, save_results=False, show_plot=False - ) - # Check appropriate return type - should be tuple (adata, figure) - self.assertIsInstance(result_no_save, tuple) - self.assertEqual(len(result_no_save), 2) - adata_result, fig_result = result_no_save - self.assertIsInstance(adata_result, ad.AnnData) - self.assertIn("scaled_arcsinh", adata_result.layers) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path, show_plot=False) - self.assertIsInstance(result_json, dict) - - # Verify normalize_features was called with correct parameters - call_args = mock_normalize.call_args - self.assertEqual(call_args[1]['input_layer'], "arcsinh") - self.assertEqual(call_args[1]['low_quantile'], 0.02) - self.assertEqual(call_args[1]['high_quantile'], 0.98) - self.assertEqual(call_args[1]['interpolation'], "nearest") - self.assertEqual(call_args[1]['output_layer'], "scaled_arcsinh") - self.assertEqual(call_args[1]['per_batch'], False) - # Empty string becomes None - self.assertIsNone(call_args[1]['annotation']) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test 1: output layer already exists - # First create adata with existing layer - adata = mock_adata() - adata.layers["scaled_arcsinh"] = adata.X.copy() - - with open(self.in_file, 'wb') as f: - pickle.dump(adata, f) - - with self.assertRaises(ValueError) as context: - run_from_json(self.params) - - # Check exact error message - expected_msg = ( - "Output Table Name 'scaled_arcsinh' already exists, " - "please rename it." - ) - self.assertEqual(str(context.exception), expected_msg) - - # Test 2: Missing annotation when per_batch is True - # Reset the input file - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) - - params_bad = self.params.copy() - params_bad["Per_Batch"] = "True" - params_bad["Annotation"] = "" # Empty annotation - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - expected_msg = ( - 'Parameter "Annotation" is required when "Per Batch" is set ' - 'to True.' + def test_quantile_scaling_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run quantile scaling and verify outputs. + + Validates: + 1. saved_files dict has 'analysis' key + 2. Pickle exists, is non-empty, contains AnnData with normalized layer + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) - self.assertEqual(str(context.exception), expected_msg) - - @patch('spac.templates.quantile_scaling_template.normalize_features') - @patch('builtins.print') - def test_console_output(self, mock_print, mock_normalize) -> None: - """Test that correct messages are printed.""" - # Mock the normalize function to return adata with the new layer - def mock_transform(adata, **kwargs): - output_layer = kwargs['output_layer'] - # Add the output layer - adata.layers[output_layer] = np.ones( - (adata.n_obs, adata.n_vars) - ) - return adata - - mock_normalize.side_effect = mock_transform - - # Test with different quantile values - params_alt = self.params.copy() - params_alt["Low_Quantile"] = "0.05" - params_alt["High_Quantile"] = "0.95" - params_alt["Per_Batch"] = "True" - params_alt["Annotation"] = "batch" - - run_from_json(params_alt, save_results=False, show_plot=False) - - # Check print statements - print_calls = [str(call[0][0]) for call in mock_print.call_args_list - if call[0]] - - # Should print quantile values - self.assertTrue( - any("High quantile used: 0.95" in msg for msg in print_calls) - ) - self.assertTrue( - any("Low quantile used: 0.05" in msg for msg in print_calls) - ) - - # Verify function was called with per_batch=True - call_args = mock_normalize.call_args - self.assertEqual(call_args[1]['per_batch'], True) - self.assertEqual(call_args[1]['annotation'], "batch") + + self.assertIsInstance(saved_files, dict) + self.assertIn("analysis", saved_files) + + pkl_path = Path(saved_files["analysis"]) + self.assertTrue(pkl_path.exists()) + self.assertGreater(pkl_path.stat().st_size, 0) + + with open(pkl_path, "rb") as f: + result_adata = pickle.load(f) + self.assertIsInstance(result_adata, ad.AnnData) + # quantile scaling creates a layer named "quantile__" + layer_names = list(result_adata.layers.keys()) + self.assertGreater(len(layer_names), 0) + + mem_result = run_from_json(self.json_file, save_to_disk=False) + # save_to_disk=False returns (adata, fig) tuple + self.assertIsNotNone(mem_result) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_relational_heatmap_template.py b/tests/templates/test_relational_heatmap_template.py index 2620656a..8c2db32a 100644 --- a/tests/templates/test_relational_heatmap_template.py +++ b/tests/templates/test_relational_heatmap_template.py @@ -1,5 +1,10 @@ # tests/templates/test_relational_heatmap_template.py -"""Unit tests for the Relational Heatmap template.""" +""" +Real (non-mocked) unit test for the Relational Heatmap template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,17 +12,14 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock, Mock +from pathlib import Path import matplotlib -import matplotlib.pyplot as plt -matplotlib.use("Agg") # Headless backend for CI +matplotlib.use("Agg") import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -26,201 +28,112 @@ from spac.templates.relational_heatmap_template import run_from_json -def mock_adata(n_cells: int = 10) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 8 cells, 3 genes, 2 groups for heatmap.""" + rng = np.random.default_rng(42) + X = rng.integers(1, 20, size=(8, 3)).astype(float) obs = pd.DataFrame({ - "phenograph_k60_r1": (["cluster1", "cluster2", "cluster3"] * - ((n_cells + 2) // 3))[:n_cells], - "renamed_phenotypes": (["phenotype_A", "phenotype_B"] * - ((n_cells + 1) // 2))[:n_cells] + "cell_type": ["A", "A", "B", "B", "A", "A", "B", "B"], }) - x_mat = rng.normal(size=(n_cells, 3)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = ["Gene1", "Gene2", "Gene3"] - return adata + var = pd.DataFrame(index=["Gene_0", "Gene_1", "Gene_2"]) + return ad.AnnData(X=X, obs=obs, var=var) class TestRelationalHeatmapTemplate(unittest.TestCase): - """Unit tests for the Relational Heatmap template.""" + """Real (non-mocked) tests for the relational heatmap template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "relational_heatmap" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters from NIDAP template - self.params = { + params = { "Upstream_Analysis": self.in_file, - "Source_Annotation_Name": "phenograph_k60_r1", - "Target_Annotation_Name": "renamed_phenotypes", - "Colormap": "darkmint", - "Figure_Width_inch": 8, - "Figure_Height_inch": 10, - "Figure_DPI": 300, + "Source_Annotation_Name": "cell_type", + "Target_Annotation_Name": "cell_type", + "Figure_Width_inch": 6, + "Figure_Height_inch": 4, + "Figure_DPI": 72, "Font_Size": 8, - "Output_File": self.out_file, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "figures": {"type": "directory", "name": "figures_dir"}, + "html": {"type": "directory", "name": "html_dir"}, + "dataframe": {"type": "file", "name": "dataframe.csv"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - @patch('spac.templates.relational_heatmap_template.relational_heatmap') - @patch('plotly.io.write_image') - @patch('matplotlib.pyplot.show') # Mock plt.show() - def test_complete_io_workflow( - self, mock_plt_show, mock_write_image, mock_relational - ) -> None: - """Single I/O test covering input/output scenarios.""" - # Mock the relational_heatmap function - mock_fig = Mock() - mock_fig.show = Mock() # Mock the fig.show() method - - mock_df = pd.DataFrame({ - 'source': ['cluster1', 'cluster2'], - 'target': ['phenotype_A', 'phenotype_B'], - 'value': [5, 3] - }) - - mock_relational.return_value = { - 'file_name': 'relational_heatmap.csv', - 'data': mock_df, - 'figure': mock_fig - } - - # Mock the plotly write_image to create a dummy image - def create_dummy_image(fig, path, **kwargs): - # Create a minimal PNG file - # Ensure file path exists (for NamedTemporaryFile) - if not os.path.exists(path): - # Create parent directory if needed - os.makedirs(os.path.dirname(path), exist_ok=True) - fig_dummy, ax = plt.subplots(figsize=(1, 1)) - ax.text(0.5, 0.5, 'test', ha='center', va='center') - plt.savefig(path, dpi=72) - plt.close(fig_dummy) - - mock_write_image.side_effect = create_dummy_image - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - # Should have both CSV and PNG files - self.assertEqual(len(result), 2) - csv_files = [f for f in result.keys() if f.endswith('.csv')] - png_files = [f for f in result.keys() if f.endswith('.png')] - self.assertEqual(len(csv_files), 1) - self.assertEqual(len(png_files), 1) - - # Test 2: Run without saving - result_no_save = run_from_json( - self.params, save_results=False - ) - # Check appropriate return type - should be (figure, dataframe) - self.assertIsInstance(result_no_save, tuple) - self.assertEqual(len(result_no_save), 2) - fig, df = result_no_save - self.assertIsInstance(df, pd.DataFrame) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - @patch('matplotlib.pyplot.show') # Mock plt.show() - def test_error_validation(self, mock_plt_show) -> None: - """Test exact error message for invalid parameters.""" - # Test with None annotations (should be handled by text_to_value) - params_none = self.params.copy() - params_none["Source_Annotation_Name"] = "None" - params_none["Target_Annotation_Name"] = "None" - - with patch('spac.templates.relational_heatmap_template.' - 'relational_heatmap') as mock_rel: - # The template should pass None values to the function - mock_rel.return_value = { - 'file_name': 'test.csv', - 'data': pd.DataFrame(), - 'figure': Mock(show=Mock()) # Mock fig.show() - } - - # Mock write_image to create a dummy file - def create_dummy_image(fig, path, **kwargs): - # Create a minimal PNG file - # Ensure file path exists (for NamedTemporaryFile) - if not os.path.exists(path): - # Create parent directory if needed - os.makedirs(os.path.dirname(path), exist_ok=True) - fig_dummy, ax = plt.subplots(figsize=(1, 1)) - ax.text(0.5, 0.5, 'test', ha='center', va='center') - plt.savefig(path, dpi=72) - plt.close(fig_dummy) - - with patch('plotly.io.write_image', - side_effect=create_dummy_image): - run_from_json(params_none) - - # Verify None was passed - call_args = mock_rel.call_args - self.assertIsNone(call_args[1]['source_annotation']) - self.assertIsNone(call_args[1]['target_annotation']) - - @patch('spac.templates.relational_heatmap_template.relational_heatmap') - @patch('plotly.io.write_image') - @patch('matplotlib.pyplot.show') # Mock plt.show() - def test_function_calls( - self, mock_plt_show, mock_write_image, mock_relational - ) -> None: - """Test that main function is called with correct parameters.""" - # Mock the main function - mock_relational.return_value = { - 'file_name': 'test.csv', - 'data': pd.DataFrame({'a': [1, 2]}), - 'figure': Mock(show=Mock()) # Mock fig.show() - } - - # Mock write_image to create a dummy file - def create_dummy_image(fig, path, **kwargs): - # Create a minimal PNG file - # Ensure file path exists (for NamedTemporaryFile) - if not os.path.exists(path): - # Create parent directory if needed - os.makedirs(os.path.dirname(path), exist_ok=True) - fig_dummy, ax = plt.subplots(figsize=(1, 1)) - ax.text(0.5, 0.5, 'test', ha='center', va='center') - plt.savefig(path, dpi=72) - plt.close(fig_dummy) - - mock_write_image.side_effect = create_dummy_image - - run_from_json(self.params, save_results=False) - - # Verify function was called correctly - mock_relational.assert_called_once() - call_args = mock_relational.call_args - - # Check specific parameters - self.assertEqual( - call_args[1]['source_annotation'], 'phenograph_k60_r1' + def test_relational_heatmap_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run relational heatmap with show_static_image=False + (default). + + Validates: + 1. saved_files dict has 'html' key (interactive HTML is default output) + 2. HTML file exists and is non-empty + 3. No 'figures' key when show_static_image=False + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) - self.assertEqual( - call_args[1]['target_annotation'], 'renamed_phenotypes' + + self.assertIsInstance(saved_files, dict) + self.assertIn("html", saved_files) + + html_paths = saved_files["html"] + self.assertGreaterEqual(len(html_paths), 1) + for html_path in html_paths: + html_file = Path(html_path) + self.assertTrue(html_file.exists()) + self.assertGreater(html_file.stat().st_size, 0) + + # When show_static_image defaults to False, no figures produced + self.assertNotIn("figures", saved_files) + + def test_relational_heatmap_with_static_image(self) -> None: + """ + End-to-end I/O test: run relational heatmap with show_static_image=True. + + Validates: + 1. saved_files dict has both 'figures' and 'html' keys + 2. Figure PNG and HTML files exist and are non-empty + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, + show_static_image=True, ) - self.assertEqual(call_args[1]['color_map'], 'darkmint') - self.assertEqual(call_args[1]['font_size'], 8) + + self.assertIsInstance(saved_files, dict) + self.assertIn("figures", saved_files) + self.assertIn("html", saved_files) + + figure_paths = saved_files["figures"] + self.assertGreaterEqual(len(figure_paths), 1) + for fig_path in figure_paths: + fig_file = Path(fig_path) + self.assertTrue(fig_file.exists()) + self.assertGreater(fig_file.stat().st_size, 0) + + html_paths = saved_files["html"] + self.assertGreaterEqual(len(html_paths), 1) + for html_path in html_paths: + html_file = Path(html_path) + self.assertTrue(html_file.exists()) + self.assertGreater(html_file.stat().st_size, 0) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_rename_labels_template.py b/tests/templates/test_rename_labels_template.py index 7d9d1e7d..41842124 100644 --- a/tests/templates/test_rename_labels_template.py +++ b/tests/templates/test_rename_labels_template.py @@ -1,5 +1,10 @@ # tests/templates/test_rename_labels_template.py -"""Unit tests for the Rename Labels template.""" +""" +Real (non-mocked) unit test for the Rename Labels template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,13 +12,11 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -22,147 +25,85 @@ from spac.templates.rename_labels_template import run_from_json -def mock_adata(n_cells: int = 10) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) - obs = pd.DataFrame({ - "phenograph_k60_r1": [str(i % 3) for i in range(n_cells)], - "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] - }) - x_mat = rng.normal(size=(n_cells, 3)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = ["Gene1", "Gene2", "Gene3"] - return adata +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 4 cells with cell_type annotation to rename.""" + rng = np.random.default_rng(42) + X = rng.random((4, 2)) + obs = pd.DataFrame({"cell_type": ["A", "B", "A", "B"]}) + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + return ad.AnnData(X=X, obs=obs, var=var) class TestRenameLabelsTemplate(unittest.TestCase): - """Unit tests for the Rename Labels template.""" + """Real (non-mocked) tests for the rename labels template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.mapping_file = os.path.join( - self.tmp_dir.name, "mapping.csv" - ) - self.out_file = "output" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Create mapping CSV - pandas will read these as integers + # Create mapping CSV: old_label -> new_label mapping_df = pd.DataFrame({ - 'Original': [0, 1, 2], - 'New': ['TypeA', 'TypeB', 'TypeC'] + "Original": ["A", "B"], + "New": ["Alpha", "Beta"], }) + self.mapping_file = os.path.join(self.tmp_dir.name, "mapping.csv") mapping_df.to_csv(self.mapping_file, index=False) - # Minimal parameters from NIDAP template - self.params = { + params = { "Upstream_Analysis": self.in_file, + "Source_Annotation": "cell_type", "Cluster_Mapping_Dictionary": self.mapping_file, - "Source_Annotation": "phenograph_k60_r1", - "New_Annotation": "renamed_phenotypes", - "Output_File": self.out_file, + "New_Annotation": "cell_type_renamed", + "Output_Directory": self.tmp_dir.name, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - # Verify file was saved - self.assertTrue(len(result) > 0) - - # Test 2: Run without saving - result_no_save = run_from_json(self.params, save_results=False) - # Check appropriate return type based on template - self.assertIsInstance(result_no_save, ad.AnnData) - # Verify new annotation exists - self.assertIn("renamed_phenotypes", result_no_save.obs.columns) - # Verify mapping was applied - unique_labels = result_no_save.obs["renamed_phenotypes"].unique() - self.assertIn("TypeA", unique_labels) - self.assertIn("TypeB", unique_labels) - self.assertIn("TypeC", unique_labels) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test missing required mapping columns - bad_mapping_df = pd.DataFrame({ - 'Wrong': ['0', '1', '2'], - 'Columns': ['TypeA', 'TypeB', 'TypeC'] - }) - bad_mapping_file = os.path.join( - self.tmp_dir.name, "bad_mapping.csv" - ) - bad_mapping_df.to_csv(bad_mapping_file, index=False) - - params_bad = self.params.copy() - params_bad["Cluster_Mapping_Dictionary"] = bad_mapping_file - - with self.assertRaises(KeyError) as context: - run_from_json(params_bad) - - # Check that error occurs when accessing 'Original' column - self.assertIn("Original", str(context.exception)) - - @patch('spac.templates.rename_labels_template.rename_annotations') - def test_function_calls(self, mock_rename) -> None: - """Test that main function is called with correct parameters.""" - # Mock the rename_annotations function to simulate its effect - def side_effect_rename(adata, src_annotation, dest_annotation, - mappings): - # Simulate what rename_annotations does - # Convert string values to int if mapping keys are int - if all(isinstance(k, int) for k in mappings.keys()): - adata.obs[dest_annotation] = ( - adata.obs[src_annotation].astype(int).map(mappings) - ) - else: - adata.obs[dest_annotation] = ( - adata.obs[src_annotation].map(mappings) - ) - return None - - mock_rename.side_effect = side_effect_rename - - # Run the template - result = run_from_json(self.params, save_results=False) - - # Verify function was called correctly - mock_rename.assert_called_once() - call_args = mock_rename.call_args - - # Check that AnnData was passed - self.assertIsInstance(call_args[0][0], ad.AnnData) - - # Check keyword arguments - self.assertEqual( - call_args[1]['src_annotation'], "phenograph_k60_r1" + def test_rename_labels_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run rename labels and verify outputs. + + Validates: + 1. saved_files dict has 'analysis' key + 2. Pickle exists, is non-empty, contains AnnData + 3. Renamed annotation column is present with new values + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) + + self.assertIsInstance(saved_files, dict) + self.assertIn("analysis", saved_files) + + pkl_path = Path(saved_files["analysis"]) + self.assertTrue(pkl_path.exists()) + self.assertGreater(pkl_path.stat().st_size, 0) + + with open(pkl_path, "rb") as f: + result_adata = pickle.load(f) + self.assertIsInstance(result_adata, ad.AnnData) + self.assertIn("cell_type_renamed", result_adata.obs.columns) self.assertEqual( - call_args[1]['dest_annotation'], "renamed_phenotypes" + set(result_adata.obs["cell_type_renamed"].unique()), + {"Alpha", "Beta"}, ) - expected_mappings = {0: 'TypeA', 1: 'TypeB', 2: 'TypeC'} - self.assertEqual(call_args[1]['mappings'], expected_mappings) + + mem_adata = run_from_json(self.json_file, save_to_disk=False) + self.assertIsInstance(mem_adata, ad.AnnData) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_ripley_l_template.py b/tests/templates/test_ripley_l_template.py index bde5a9de..1bc926ac 100644 --- a/tests/templates/test_ripley_l_template.py +++ b/tests/templates/test_ripley_l_template.py @@ -1,5 +1,10 @@ # tests/templates/test_ripley_l_template.py -"""Unit-tests for the Ripley-L template.""" +""" +Real (non-mocked) unit test for the Ripley L Calculation template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,7 +12,8 @@ import sys import tempfile import unittest -from unittest.mock import patch, MagicMock +from pathlib import Path + import anndata as ad import numpy as np import pandas as pd @@ -16,229 +22,86 @@ os.path.dirname(os.path.realpath(__file__)) + "/../../src" ) -from spac.templates.ripley_l_template import run_from_json +from spac.templates.ripley_l_calculation_template import run_from_json -def mock_adata(n_cells: int = 40) -> ad.AnnData: - """Return a tiny synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) - obs = pd.DataFrame( - { - "renamed_phenotypes": np.where( - rng.random(n_cells) > 0.5, "B cells", "CD8 T cells" - ) - } - ) - x_mat = rng.normal(size=(n_cells, 3)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.obsm["spatial"] = rng.random((n_cells, 2)) * 300.0 +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 20 cells with spatial coords for Ripley L.""" + rng = np.random.default_rng(42) + X = rng.random((20, 2)) + obs = pd.DataFrame({ + "cell_type": (["A"] * 10) + (["B"] * 10), + }) + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + spatial = rng.random((20, 2)) * 100 + adata = ad.AnnData(X=X, obs=obs, var=var) + adata.obsm["spatial"] = spatial return adata class TestRipleyLTemplate(unittest.TestCase): - """Light-weight sanity checks for the Ripley-L template.""" + """Real (non-mocked) tests for the Ripley L calculation template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - self.out_file = os.path.join(self.tmp_dir.name, "output.pickle") - # Save as pickle - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - self.params = { + params = { "Upstream_Analysis": self.in_file, - "Radii": [0, 50, 100], - "Annotation": "renamed_phenotypes", - "Center_Phenotype": "B cells", - "Neighbor_Phenotype": "CD8 T cells", - "Output_path": self.out_file, + "Radii": [5, 10, 20], + "Annotation": "cell_type", + "Center_Phenotype": "A", + "Neighbor_Phenotype": "B", "Stratify_By": "None", - "Number_of_Simulations": 100, - "Area": "None", + "Number_of_Simulations": 5, "Seed": 42, "Spatial_Key": "spatial", - "Edge_Correction": True + "Edge_Correction": True, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - @patch('spac.templates.ripley_l_template.ripley_l') - def test_run_from_dict_with_save(self, mock_ripley_l) -> None: - """Test run_from_json with dict parameters and file saving.""" - # Mock the ripley_l function - def mock_ripley_side_effect(adata, **kwargs): - # Simulate what ripley_l does - adds results to adata.uns - phenotypes = kwargs.get('phenotypes', ['B cells', 'CD8 T cells']) - key = f"ripley_l_{phenotypes[0]}_{phenotypes[1]}" - adata.uns[key] = { - "radius": kwargs.get('distances', [0, 50, 100]), - "ripley_l": [0.0, 1.2, 2.5], - "simulations": [[0.0, 0.8, 1.9], [0.0, 1.1, 2.3]], - } - return None - - mock_ripley_l.side_effect = mock_ripley_side_effect - - # Run with save_results=True (default) - saved_files = run_from_json(self.params) - - # Check that ripley_l was called with correct parameters - mock_ripley_l.assert_called_once() - call_args = mock_ripley_l.call_args - - # Verify the call arguments - self.assertEqual(call_args[1]['annotation'], "renamed_phenotypes") - self.assertEqual(call_args[1]['phenotypes'], ["B cells", "CD8 T cells"]) - self.assertEqual(call_args[1]['distances'], [0.0, 50.0, 100.0]) - self.assertEqual(call_args[1]['n_simulations'], 100) - self.assertEqual(call_args[1]['seed'], 42) - self.assertEqual(call_args[1]['spatial_key'], "spatial") - self.assertEqual(call_args[1]['edge_correction'], True) - - # Check that output file was created - check if any file was saved - self.assertTrue( - len(saved_files) > 0, - f"Expected files to be saved, but got {saved_files}" + def test_ripley_l_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run Ripley L calculation and verify outputs. + + Validates: + 1. saved_files dict has 'analysis' key + 2. Pickle contains AnnData with Ripley results in .uns + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) - # Check that at least one pickle file was created - pickle_files = [f for f in saved_files.values() - if f.endswith('.pickle')] - self.assertTrue( - len(pickle_files) > 0, - f"Expected at least one pickle file, got {saved_files}" - ) - - @patch('spac.templates.ripley_l_template.ripley_l') - def test_run_from_dict_without_save(self, mock_ripley_l) -> None: - """Test run_from_json with dict parameters and no file saving.""" - # Mock the ripley_l function - def mock_ripley_side_effect(adata, **kwargs): - phenotypes = kwargs.get('phenotypes', ['B cells', 'CD8 T cells']) - key = f"ripley_l_{phenotypes[0]}_{phenotypes[1]}" - adata.uns[key] = { - "radius": kwargs.get('distances', [0, 50, 100]), - "ripley_l": [0.0, 1.2, 2.5], - } - return None - - mock_ripley_l.side_effect = mock_ripley_side_effect - - # Run with save_results=False - adata_result = run_from_json(self.params, save_results=False) - - # Check that ripley_l was called - mock_ripley_l.assert_called_once() - - # Check that we got an AnnData object back - self.assertIsInstance(adata_result, ad.AnnData) - - # Check that results are in the object - ripley_key = "ripley_l_B cells_CD8 T cells" - self.assertIn(ripley_key, adata_result.uns) - - @patch('spac.templates.ripley_l_template.ripley_l') - def test_run_from_json_file(self, mock_ripley_l) -> None: - """Test run_from_json accepts a JSON file path.""" - # Mock the ripley_l function - def mock_ripley_side_effect(adata, **kwargs): - phenotypes = kwargs.get('phenotypes', ['B cells', 'CD8 T cells']) - key = f"ripley_l_{phenotypes[0]}_{phenotypes[1]}" - adata.uns[key] = { - "radius": kwargs.get('distances', [0, 50, 100]), - "ripley_l": [0.0, 1.2, 2.5], - "simulations": [[0.0, 0.8, 1.9], [0.0, 1.1, 2.3]], - } - return None - - mock_ripley_l.side_effect = mock_ripley_side_effect - - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as handle: - json.dump(self.params, handle) - - saved_files = run_from_json(json_path) - - # Verify ripley_l was called - mock_ripley_l.assert_called_once() - - # Check that save_outputs was called - mock_ripley_l.assert_called_once() - - # Check that files were saved - self.assertTrue(len(saved_files) > 0) - - @patch('spac.templates.ripley_l_template.ripley_l') - def test_radii_conversion(self, mock_ripley_l) -> None: - """Test that radii strings are converted to floats.""" - # Use string radii - params_str = self.params.copy() - params_str["Radii"] = ["0", "50", "100"] - - def mock_ripley_side_effect(adata, **kwargs): - phenotypes = kwargs.get('phenotypes', ['B cells', 'CD8 T cells']) - key = f"ripley_l_{phenotypes[0]}_{phenotypes[1]}" - adata.uns[key] = {"radius": [0, 50, 100], "ripley_l": [0, 1, 2]} - return None - - mock_ripley_l.side_effect = mock_ripley_side_effect - - run_from_json(params_str, save_results=False) - - # Check that radii were converted to floats - call_args = mock_ripley_l.call_args - self.assertEqual(call_args[1]['distances'], [0.0, 50.0, 100.0]) - - def test_invalid_radius_conversion(self) -> None: - """Test that invalid radius values raise appropriate errors.""" - params_bad = self.params.copy() - params_bad["Radii"] = ["0", "50", "invalid"] - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - expected_msg = "Failed to convert value: 'invalid' to float" - self.assertIn(expected_msg, str(context.exception)) - - def test_parameter_validation(self) -> None: - """Test that missing required parameters raise errors.""" - params_missing = self.params.copy() - del params_missing["Center_Phenotype"] - - with self.assertRaises(KeyError) as context: - run_from_json(params_missing) - - self.assertIn("Center_Phenotype", str(context.exception)) - - @patch('spac.templates.ripley_l_template.ripley_l') - def test_regions_parameter(self, mock_ripley_l) -> None: - """Test that regions parameter is processed correctly.""" - params_regions = self.params.copy() - params_regions["Stratify_By"] = "tumor_region" - - mock_ripley_l.side_effect = lambda adata, **kwargs: None - - run_from_json(params_regions, save_results=False) - - # Check that regions was passed correctly (not as "None") - call_args = mock_ripley_l.call_args - self.assertEqual(call_args[1]['regions'], "tumor_region") + self.assertIsInstance(saved_files, dict) + self.assertIn("analysis", saved_files) - def test_pickle_output_format(self) -> None: - """Test that output defaults to pickle format.""" - params = self.params.copy() - params["Output_File"] = "results.dat" # No extension + pkl_path = Path(saved_files["analysis"]) + self.assertTrue(pkl_path.exists()) + self.assertGreater(pkl_path.stat().st_size, 0) - with patch('spac.templates.ripley_l_template.ripley_l'): - saved_files = run_from_json(params) + with open(pkl_path, "rb") as f: + result_adata = pickle.load(f) + self.assertIsInstance(result_adata, ad.AnnData) + # Ripley results stored in .uns + self.assertGreater(len(result_adata.uns), 0) - # Should save as pickle by default - pickle_files = [f for f in saved_files.values() - if '.pickle' in str(f)] - self.assertTrue(len(pickle_files) > 0) + mem_adata = run_from_json(self.json_file, save_to_disk=False) + self.assertIsInstance(mem_adata, ad.AnnData) if __name__ == "__main__": diff --git a/tests/templates/test_sankey_plot_template.py b/tests/templates/test_sankey_plot_template.py index 198d1ba4..dc73a2c0 100644 --- a/tests/templates/test_sankey_plot_template.py +++ b/tests/templates/test_sankey_plot_template.py @@ -1,5 +1,10 @@ # tests/templates/test_sankey_plot_template.py -"""Unit tests for the Sankey Plot template.""" +""" +Real (non-mocked) unit test for the Sankey Plot template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,16 +12,14 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import matplotlib -matplotlib.use("Agg") # Headless backend for CI +matplotlib.use("Agg") import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -25,175 +28,105 @@ from spac.templates.sankey_plot_template import run_from_json -def mock_adata(n_cells: int = 20) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) - - # Create observations with source and target annotations - # Ensure balanced distribution for sankey plot - source_types = ["ClusterA", "ClusterB", "ClusterC"] - target_types = ["PhenotypeX", "PhenotypeY", "PhenotypeZ"] - - # Create source annotations with proper repetition - source_pattern = source_types * ( - (n_cells + len(source_types) - 1) // len(source_types) - ) - source_annotation = source_pattern[:n_cells] - - # Create target annotations with some mixing - target_annotation = [] - for i in range(n_cells): - if i % 3 == 0: - target_annotation.append(target_types[0]) - elif i % 3 == 1: - target_annotation.append(target_types[1]) - else: - target_annotation.append(target_types[2]) - +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 8 cells with two annotation columns for Sankey.""" + rng = np.random.default_rng(42) + X = rng.random((8, 2)) obs = pd.DataFrame({ - "phenograph_k60_r1": source_annotation, - "renamed_phenotypes": target_annotation + "cell_type": ["A", "A", "B", "B", "A", "A", "B", "B"], + "cluster": ["1", "2", "1", "2", "1", "2", "1", "2"], }) - - x_mat = rng.normal(size=(n_cells, 3)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = ["Marker1", "Marker2", "Marker3"] - - return adata + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + return ad.AnnData(X=X, obs=obs, var=var) class TestSankeyPlotTemplate(unittest.TestCase): - """Unit tests for the Sankey Plot template.""" + """Real (non-mocked) tests for the sankey plot template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "sankey" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters from NIDAP template - self.params = { + params = { "Upstream_Analysis": self.in_file, - "Source_Annotation_Name": "phenograph_k60_r1", - "Target_Annotation_Name": "renamed_phenotypes", - "Source_Annotation_Color_Map": "tab20", - "Target_Annotation_Color_Map": "tab20b", + "Source_Annotation_Name": "cell_type", + "Target_Annotation_Name": "cluster", "Figure_Width_inch": 6, "Figure_Height_inch": 6, - "Figure_DPI": 300, - "Font_Size": 12, - "Output_File": self.out_file, + "Font_Size": 10, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "figures": {"type": "directory", "name": "figures_dir"}, + "html": {"type": "directory", "name": "html_dir"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - @patch('spac.templates.sankey_plot_template.sankey_plot') - @patch('plotly.io.write_image') - @patch('plotly.io.write_html') - @patch('matplotlib.pyplot.imread') - def test_complete_io_workflow( - self, mock_imread, mock_write_html, mock_write_image, mock_sankey - ) -> None: - """Single I/O test covering input/output scenarios.""" - # Mock the sankey_plot function to return a plotly figure - mock_fig = MagicMock() - mock_fig.update_layout = MagicMock() - mock_fig.show = MagicMock() - mock_sankey.return_value = mock_fig - - # Mock plt.imread to return a dummy image array - mock_imread.return_value = np.zeros((100, 100, 3)) - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters - result = run_from_json(self.params, show_plot=False) - self.assertIsInstance(result, dict) - - # Verify multiple files were saved - self.assertIn("sankey_static.png", result) - self.assertIn("sankey_interactive.html", result) - self.assertIn("sankey_diagram.png", result) - - # Test 2: Run without saving - result_no_save = run_from_json( - self.params, save_results=False, show_plot=False - ) - # Should return None for multi-plot template - self.assertIsNone(result_no_save) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path, show_plot=False) - self.assertIsInstance(result_json, dict) - - # Verify sankey_plot was called with correct parameters - mock_sankey.assert_called() - call_args = mock_sankey.call_args - self.assertEqual( - call_args[1]['source_annotation'], "phenograph_k60_r1" + def test_sankey_plot_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run sankey plot with show_static_image=False + (default). + + Validates: + 1. saved_files dict has 'html' key (interactive HTML is default) + 2. HTML output files exist and are non-empty + 3. No 'figures' key when show_static_image=False + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) - self.assertEqual( - call_args[1]['target_annotation'], "renamed_phenotypes" + + self.assertIsInstance(saved_files, dict) + self.assertIn("html", saved_files) + + html_paths = saved_files["html"] + self.assertGreaterEqual(len(html_paths), 1) + for p in html_paths: + pf = Path(p) + self.assertTrue(pf.exists()) + self.assertGreater(pf.stat().st_size, 0) + + # When show_static_image defaults to False, no figures produced + self.assertNotIn("figures", saved_files) + + def test_sankey_plot_with_static_image(self) -> None: + """ + End-to-end I/O test: run sankey plot with show_static_image=True. + + Validates: + 1. saved_files dict has both 'figures' and 'html' keys + 2. Figure PNG and HTML files exist and are non-empty + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, + show_static_image=True, ) - self.assertEqual(call_args[1]['source_color_map'], "tab20") - self.assertEqual(call_args[1]['target_color_map'], "tab20b") - self.assertEqual(call_args[1]['sankey_font'], 12) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test missing annotation column - params_bad = self.params.copy() - params_bad["Source_Annotation_Name"] = "nonexistent_column" - - # This should trigger an error in the sankey_plot function - # This test will check that parameters are processed correctly - with patch('spac.templates.sankey_plot_template.sankey_plot') as \ - mock_sankey: - mock_sankey.side_effect = KeyError("nonexistent_column") - - with self.assertRaises(KeyError) as context: - run_from_json(params_bad) - - self.assertIn("nonexistent_column", str(context.exception)) - - @patch('spac.templates.sankey_plot_template.sankey_plot') - @patch('plotly.io.write_image') - @patch('plotly.io.write_html') - @patch('matplotlib.pyplot.imread') - def test_function_calls( - self, mock_imread, mock_write_html, mock_write_image, mock_sankey - ) -> None: - """Test that main function is called with correct parameters.""" - # Mock the plotly figure - mock_fig = MagicMock() - mock_sankey.return_value = mock_fig - - # Mock plt.imread to return a dummy image array - mock_imread.return_value = np.zeros((100, 100, 3)) - - # Test with None annotations (should use text_to_value) - params_none = self.params.copy() - params_none["Source_Annotation_Name"] = "None" - params_none["Target_Annotation_Name"] = "None" - - run_from_json(params_none, save_results=False, show_plot=False) - - # Verify function was called with None values - call_args = mock_sankey.call_args - self.assertIsNone(call_args[1]['source_annotation']) - self.assertIsNone(call_args[1]['target_annotation']) + + self.assertIsInstance(saved_files, dict) + self.assertIn("figures", saved_files) + self.assertIn("html", saved_files) + + for key in ["html", "figures"]: + paths = saved_files[key] + self.assertGreaterEqual(len(paths), 1) + for p in paths: + pf = Path(p) + self.assertTrue(pf.exists()) + self.assertGreater(pf.stat().st_size, 0) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_select_values_template.py b/tests/templates/test_select_values_template.py index edf343c0..abfd4c8d 100644 --- a/tests/templates/test_select_values_template.py +++ b/tests/templates/test_select_values_template.py @@ -1,17 +1,23 @@ # tests/templates/test_select_values_template.py -"""Unit tests for the Select Values template.""" +""" +Real (non-mocked) unit test for the Select Values template. + +Validates template I/O behaviour only: + - Expected output files are produced on disk + - Filenames follow the convention + - Output artifacts are non-empty + +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os -import pickle import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -20,131 +26,87 @@ from spac.templates.select_values_template import run_from_json -def mock_dataframe(n_rows: int = 10) -> pd.DataFrame: - """Return a minimal synthetic DataFrame for fast tests.""" +def _make_tiny_dataframe() -> pd.DataFrame: + """ + Minimal synthetic DataFrame for value filtering. + + 6 rows, 3 cell types -- enough to test include-based selection. + """ return pd.DataFrame({ - "file_name": [f"Halo_Synthetic_Example_{i % 3 + 1}" - for i in range(n_rows)], - "cell_type": (["TypeA", "TypeB"] * ((n_rows + 1) // 2))[:n_rows], - "marker_value": range(n_rows) + "cell_type": ["A", "B", "C", "A", "B", "C"], + "marker": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0], }) class TestSelectValuesTemplate(unittest.TestCase): - """Unit tests for the Select Values template.""" + """Real (non-mocked) tests for the select values template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.csv" - ) - self.out_file = "select_values.csv" + self.in_file = os.path.join(self.tmp_dir.name, "input.csv") - # Save minimal mock data - mock_df = mock_dataframe() - mock_df.to_csv(self.in_file, index=False) + _make_tiny_dataframe().to_csv(self.in_file, index=False) - # Minimal parameters from NIDAP template - self.params = { + params = { "Upstream_Dataset": self.in_file, - "Annotation_of_Interest": "file_name", - "Label_s_of_Interest": ["Halo_Synthetic_Example_1"], - "Output_File": self.out_file, + "Annotation_of_Interest": "cell_type", + "Label_s_of_Interest": ["A", "B"], + "Output_Directory": self.tmp_dir.name, + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - self.assertIn(self.out_file, result) - - # Verify the output file exists and has correct content - output_path = result[self.out_file] - self.assertTrue(os.path.exists(output_path)) - - # Read and verify filtered data - filtered_df = pd.read_csv(output_path) - self.assertEqual( - filtered_df["file_name"].unique().tolist(), - ["Halo_Synthetic_Example_1"] - ) - - # Test 2: Run without saving - result_no_save = run_from_json(self.params, save_results=False) - self.assertIsInstance(result_no_save, pd.DataFrame) - self.assertEqual( - result_no_save["file_name"].unique().tolist(), - ["Halo_Synthetic_Example_1"] - ) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test with non-existent annotation - params_bad = self.params.copy() - params_bad["Annotation_of_Interest"] = "non_existent_column" - - with self.assertRaises(Exception): - # The select_values function should raise an error - run_from_json(params_bad) - - @patch('spac.templates.select_values_template.select_values') - def test_function_calls(self, mock_select) -> None: - """Test that main function is called with correct parameters.""" - # Mock the select_values function - mock_df = mock_dataframe(3) - mock_select.return_value = mock_df - - run_from_json(self.params) - - # Verify function was called correctly - mock_select.assert_called_once() - call_args = mock_select.call_args - - # Check the arguments - self.assertEqual(call_args[1]['annotation'], "file_name") + def test_select_values_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run select values template and verify + output artifacts. + + Validates: + 1. saved_files is a dict with 'dataframe' key + 2. Output CSV exists and is non-empty + 3. Only selected values (A, B) remain in the output + """ + # -- Act (save_to_disk=True) ----------------------------------- + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, + ) + + # -- Assert: return type --------------------------------------- + self.assertIsInstance(saved_files, dict) + + # -- Assert: CSV file exists and is non-empty ------------------ + self.assertIn("dataframe", saved_files) + csv_path = Path(saved_files["dataframe"]) + self.assertTrue(csv_path.exists(), f"CSV not found: {csv_path}") + self.assertGreater(csv_path.stat().st_size, 0) + + # -- Assert: only selected values remain ----------------------- + result_df = pd.read_csv(csv_path) + self.assertEqual(len(result_df), 4) self.assertEqual( - call_args[1]['values'], - ["Halo_Synthetic_Example_1"] + set(result_df["cell_type"].unique()), {"A", "B"} + ) + + # -- Act (save_to_disk=False) ---------------------------------- + mem_df = run_from_json( + self.json_file, + save_to_disk=False, ) - def test_multiple_file_formats(self) -> None: - """Test loading from different file formats.""" - # Test CSV (already covered above) - - # Test pickle format - pickle_file = os.path.join(self.tmp_dir.name, "input.pkl") - mock_df = mock_dataframe() - with open(pickle_file, 'wb') as f: - pickle.dump(mock_df, f) - - params_pickle = self.params.copy() - params_pickle["Upstream_Dataset"] = pickle_file - - result = run_from_json(params_pickle, save_results=False) - self.assertIsInstance(result, pd.DataFrame) - - # Test direct DataFrame input - params_df = self.params.copy() - params_df["Upstream_Dataset"] = mock_df - - result = run_from_json(params_df, save_results=False) - self.assertIsInstance(result, pd.DataFrame) + # -- Assert: in-memory return is DataFrame --------------------- + self.assertIsInstance(mem_df, pd.DataFrame) + self.assertEqual(len(mem_df), 4) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_setup_analysis_template.py b/tests/templates/test_setup_analysis_template.py index 6fd2e056..79fe9fa3 100644 --- a/tests/templates/test_setup_analysis_template.py +++ b/tests/templates/test_setup_analysis_template.py @@ -1,17 +1,22 @@ # tests/templates/test_setup_analysis_template.py -"""Unit tests for the Setup Analysis template.""" +""" +Real (non-mocked) unit test for the Setup Analysis template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os +import pickle import sys import tempfile import unittest -import pickle from pathlib import Path -import pandas as pd + import anndata as ad import numpy as np -from unittest.mock import patch, MagicMock +import pandas as pd sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -20,279 +25,82 @@ from spac.templates.setup_analysis_template import run_from_json -def create_test_dataframe(n_cells: int = 10) -> pd.DataFrame: - """Create minimal test dataframe for setup analysis.""" - rng = np.random.default_rng(0) - df = pd.DataFrame({ - 'CellID': range(1, n_cells + 1), - 'X_centroid': rng.uniform(0, 100, n_cells), - 'Y_centroid': rng.uniform(0, 100, n_cells), - 'CD25': rng.normal(10, 2, n_cells), - 'CD3D': rng.normal(15, 3, n_cells), - 'CD45': rng.normal(20, 4, n_cells), - 'CD4': rng.normal(12, 2.5, n_cells), - 'CD8A': rng.normal(8, 2, n_cells), - 'broad_cell_type': rng.choice( - ['T cells', 'B cells'], n_cells - ), - 'detailed_cell_type': rng.choice( - ['CD4 T cells', 'CD8 T cells', 'B cells'], n_cells - ) +def _make_tiny_dataframe() -> pd.DataFrame: + """ + Minimal synthetic DataFrame simulating raw cell data. + + 4 cells with spatial coordinates, features, and an annotation column. + """ + return pd.DataFrame({ + "Gene_0": [1.0, 2.0, 3.0, 4.0], + "Gene_1": [5.0, 6.0, 7.0, 8.0], + "X_coord": [10.0, 20.0, 30.0, 40.0], + "Y_coord": [11.0, 21.0, 31.0, 41.0], + "cell_type": ["A", "B", "A", "B"], }) - return df class TestSetupAnalysisTemplate(unittest.TestCase): - """Unit tests for the Setup Analysis template.""" + """Real (non-mocked) tests for the setup analysis template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - - # Create test data - self.test_df = create_test_dataframe() - self.csv_file = os.path.join( - self.tmp_dir.name, "test_data.csv" - ) - self.test_df.to_csv(self.csv_file, index=False) - - # Minimal parameters - self.params = { - "Upstream_Dataset": self.csv_file, - "Features_to_Analyze": ["CD25", "CD3D", "CD45", "CD4", "CD8A"], - "Feature_Regex": [], - "X_Coordinate_Column": "X_centroid", - "Y_Coordinate_Column": "Y_centroid", - "Annotation_s_": ["broad_cell_type", "detailed_cell_type"], - "Output_File": "analysis_output.pickle" + self.in_file = os.path.join(self.tmp_dir.name, "input.csv") + + _make_tiny_dataframe().to_csv(self.in_file, index=False) + + params = { + "Upstream_Dataset": self.in_file, + "Features_to_Analyze": ["Gene_0", "Gene_1"], + "Annotation_s_": ["cell_type"], + "X_Coordinate_Column": "X_coord", + "Y_Coordinate_Column": "Y_coord", + "Output_File": "output.pickle", + "Output_Directory": self.tmp_dir.name, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - @patch('spac.templates.setup_analysis_template.ingest_cells') - def test_run_with_save(self, mock_ingest) -> None: - """Test setup analysis with file saving.""" - # Mock the ingest_cells function - mock_adata = ad.AnnData( - X=np.random.rand(10, 5), - obs=pd.DataFrame({'cell_type': ['A'] * 10}) + def test_setup_analysis_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run setup analysis and verify outputs. + + Validates: + 1. saved_files dict has 'analysis' key + 2. Pickle exists, is non-empty, contains AnnData + 3. AnnData has correct features, obs, and spatial coords + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) - mock_adata.var_names = ['CD25', 'CD3D', 'CD45', 'CD4', 'CD8A'] - mock_ingest.return_value = mock_adata - - saved_files = run_from_json(self.params) - - # Check that ingest_cells was called with correct parameters - mock_ingest.assert_called_once() - call_args = mock_ingest.call_args - - # Verify the call arguments - self.assertIsInstance(call_args[1]['dataframe'], pd.DataFrame) - self.assertEqual( - call_args[1]['x_col'], "X_centroid" - ) - self.assertEqual( - call_args[1]['y_col'], "Y_centroid" - ) - self.assertEqual( - call_args[1]['annotation'], - ["broad_cell_type", "detailed_cell_type"] - ) - - # Check regex includes feature names - regex_list = call_args[1]['regex_str'] - for feature in self.params["Features_to_Analyze"]: - self.assertIn(f"^{feature}$", regex_list) - - # Check that output file was created - self.assertIn("analysis_output.pickle", saved_files) - - @patch('spac.templates.setup_analysis_template.ingest_cells') - def test_run_without_save(self, mock_ingest) -> None: - """Test setup analysis without file saving.""" - # Mock the ingest_cells function - mock_adata = ad.AnnData( - X=np.random.rand(10, 5), - obs=pd.DataFrame({'cell_type': ['A'] * 10}) - ) - mock_adata.var_names = ['CD25', 'CD3D', 'CD45', 'CD4', 'CD8A'] - mock_ingest.return_value = mock_adata - - result = run_from_json(self.params, save_results=False) - - # Check that we got an AnnData object back - self.assertIsInstance(result, ad.AnnData) - # For AnnData, we check that it's the same object reference - self.assertIs(result, mock_adata) - - def test_annotation_none_handling(self) -> None: - """Test handling of 'None' annotation.""" - params_none = self.params.copy() - params_none["Annotation_s_"] = ["None"] - - with patch( - 'spac.templates.setup_analysis_template.ingest_cells' - ) as mock_ingest: - mock_adata = ad.AnnData(X=np.random.rand(10, 5)) - mock_adata.var_names = ['CD25', 'CD3D', 'CD45', 'CD4', 'CD8A'] - mock_ingest.return_value = mock_adata - - run_from_json(params_none, save_results=False) - - # Check that annotation was set to None - call_args = mock_ingest.call_args - self.assertIsNone(call_args[1]['annotation']) - - def test_annotation_validation_error_message(self) -> None: - """Test exact error message for invalid annotation.""" - params_bad = self.params.copy() - params_bad["Annotation_s_"] = ["broad_cell_type", "None", "other"] - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - expected_msg = 'String "None" found in the annotation list' - actual_msg = str(context.exception) - self.assertEqual(expected_msg, actual_msg) - - def test_coordinate_none_handling(self) -> None: - """Test handling of 'None' coordinates.""" - params_no_coords = self.params.copy() - params_no_coords["X_Coordinate_Column"] = "None" - params_no_coords["Y_Coordinate_Column"] = "None" - - with patch( - 'spac.templates.setup_analysis_template.ingest_cells' - ) as mock_ingest: - mock_adata = ad.AnnData(X=np.random.rand(10, 5)) - mock_adata.var_names = ['CD25', 'CD3D', 'CD45', 'CD4', 'CD8A'] - mock_ingest.return_value = mock_adata - - run_from_json(params_no_coords, save_results=False) - - # Check that coordinates were set to None - call_args = mock_ingest.call_args - self.assertIsNone(call_args[1]['x_col']) - self.assertIsNone(call_args[1]['y_col']) - - def test_json_file_input(self) -> None: - """Test with JSON file input.""" - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - with patch( - 'spac.templates.setup_analysis_template.ingest_cells' - ) as mock_ingest: - mock_adata = ad.AnnData(X=np.random.rand(10, 5)) - mock_adata.var_names = ['CD25', 'CD3D', 'CD45', 'CD4', 'CD8A'] - mock_ingest.return_value = mock_adata - - saved_files = run_from_json(json_path) - - # Check that files were saved - self.assertTrue(len(saved_files) > 0) - - def test_feature_regex_combination(self) -> None: - """Test combination of feature names and regex.""" - params_regex = self.params.copy() - params_regex["Feature_Regex"] = [".*_expression$", "DAPI.*"] - - with patch( - 'spac.templates.setup_analysis_template.ingest_cells' - ) as mock_ingest: - mock_adata = ad.AnnData(X=np.random.rand(10, 5)) - mock_adata.var_names = ['CD25', 'CD3D', 'CD45', 'CD4', 'CD8A'] - mock_ingest.return_value = mock_adata - - run_from_json(params_regex, save_results=False) - - # Check that regex includes both custom patterns and features - call_args = mock_ingest.call_args - regex_list = call_args[1]['regex_str'] - - # Should include custom regex - self.assertIn(".*_expression$", regex_list) - self.assertIn("DAPI.*", regex_list) - - # Should include feature patterns - for feature in params_regex["Features_to_Analyze"]: - self.assertIn(f"^{feature}$", regex_list) - - def test_dataframe_input(self) -> None: - """Test with DataFrame as upstream dataset.""" - params_df = self.params.copy() - params_df["Upstream_Dataset"] = self.test_df - - with patch( - 'spac.templates.setup_analysis_template.ingest_cells' - ) as mock_ingest: - mock_adata = ad.AnnData(X=np.random.rand(10, 5)) - mock_adata.var_names = ['CD25', 'CD3D', 'CD45', 'CD4', 'CD8A'] - mock_ingest.return_value = mock_adata - - run_from_json(params_df, save_results=False) - - # Check that DataFrame was passed directly - call_args = mock_ingest.call_args - pd.testing.assert_frame_equal( - call_args[1]['dataframe'], self.test_df - ) - - @patch('builtins.print') - def test_console_output(self, mock_print) -> None: - """Test console output messages.""" - with patch( - 'spac.templates.setup_analysis_template.ingest_cells' - ) as mock_ingest: - mock_adata = ad.AnnData(X=np.random.rand(10, 5)) - mock_adata.var_names = ['CD25', 'CD3D', 'CD45', 'CD4', 'CD8A'] - mock_ingest.return_value = mock_adata - - run_from_json(self.params, save_results=False) - - # Verify output messages - print_calls = [ - str(call[0][0]) for call in mock_print.call_args_list - if call[0] - ] - - # Should print "Analysis Setup:" - setup_msgs = [ - msg for msg in print_calls - if 'Analysis Setup:' in msg - ] - self.assertTrue(len(setup_msgs) > 0) - - # Should print "Schema:" - schema_msgs = [ - msg for msg in print_calls - if 'Schema:' in msg - ] - self.assertTrue(len(schema_msgs) > 0) - def test_single_feature_and_annotation(self) -> None: - """Test with single feature and annotation as strings.""" - params_single = self.params.copy() - params_single["Features_to_Analyze"] = "CD25" - params_single["Annotation_s_"] = "broad_cell_type" + self.assertIsInstance(saved_files, dict) + self.assertIn("analysis", saved_files) - with patch( - 'spac.templates.setup_analysis_template.ingest_cells' - ) as mock_ingest: - mock_adata = ad.AnnData(X=np.random.rand(10, 1)) - mock_adata.var_names = ['CD25'] - mock_ingest.return_value = mock_adata + pkl_path = Path(saved_files["analysis"]) + self.assertTrue(pkl_path.exists()) + self.assertGreater(pkl_path.stat().st_size, 0) - run_from_json(params_single, save_results=False) + with open(pkl_path, "rb") as f: + result_adata = pickle.load(f) + self.assertIsInstance(result_adata, ad.AnnData) + self.assertEqual(result_adata.n_obs, 4) + self.assertIn("cell_type", result_adata.obs.columns) + self.assertIn("spatial", result_adata.obsm) - # Check that single values were converted to lists - call_args = mock_ingest.call_args - self.assertIn("^CD25$", call_args[1]['regex_str']) - self.assertEqual( - call_args[1]['annotation'], ["broad_cell_type"] - ) + mem_adata = run_from_json(self.json_file, save_to_disk=False) + self.assertIsInstance(mem_adata, ad.AnnData) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_spatial_interaction_template.py b/tests/templates/test_spatial_interaction_template.py index 992cbc9a..e531f8c9 100644 --- a/tests/templates/test_spatial_interaction_template.py +++ b/tests/templates/test_spatial_interaction_template.py @@ -1,5 +1,10 @@ # tests/templates/test_spatial_interaction_template.py -"""Unit tests for the Spatial Interaction template.""" +""" +Real (non-mocked) unit test for the Spatial Interaction template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,16 +12,14 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import matplotlib -matplotlib.use("Agg") # Headless backend for CI +matplotlib.use("Agg") import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -25,218 +28,85 @@ from spac.templates.spatial_interaction_template import run_from_json -def mock_adata(n_cells: int = 100) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 20 cells with spatial coords for interaction.""" + rng = np.random.default_rng(42) + X = rng.random((20, 2)) obs = pd.DataFrame({ - "cell_type": ( - ["TypeA", "TypeB", "TypeC"] * ((n_cells + 2) // 3) - )[:n_cells], - "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells], - "batch": (["Batch1", "Batch2"] * ((n_cells + 1) // 2))[:n_cells] + "cell_type": (["A"] * 10) + (["B"] * 10), }) - x_mat = rng.normal(size=(n_cells, 5)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = [f"Marker{i}" for i in range(5)] - # Add spatial coordinates - required for spatial analysis - adata.obsm["spatial"] = rng.random((n_cells, 2)) * 100 + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + spatial = rng.random((20, 2)) * 100 + adata = ad.AnnData(X=X, obs=obs, var=var) + adata.obsm["spatial"] = spatial return adata class TestSpatialInteractionTemplate(unittest.TestCase): - """Unit tests for the Spatial Interaction template.""" + """Real (non-mocked) tests for the spatial interaction template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "spatial_interaction" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters from NIDAP template - self.params = { + params = { "Upstream_Analysis": self.in_file, "Annotation": "cell_type", "Spatial_Analysis_Method": "Neighborhood Enrichment", "Stratify_By": ["None"], + "K_Nearest_Neighbors": 6, + "Seed": 42, "Coordinate_Type": "None", - "Seed": "None", "Radius": "None", - "K_Nearest_Neighbors": 6, - "Figure_Width": 15, - "Figure_Height": 12, - "Figure_DPI": 200, - "Font_Size": 12, + "Figure_Width": 6, + "Figure_Height": 4, + "Figure_DPI": 72, + "Font_Size": 10, "Color_Bar_Range": "Automatic", - "Output_File": self.out_file, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "figures": {"type": "directory", "name": "figures"}, + "dataframes": {"type": "directory", "name": "matrices"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - @patch('spac.templates.spatial_interaction_template.' - 'spatial_interaction') - def test_complete_io_workflow(self, mock_spatial) -> None: - """Single I/O test covering input/output scenarios.""" - # Mock the spatial_interaction function - mock_fig = MagicMock() - mock_fig.number = 1 - mock_ax = MagicMock() - mock_ax.get_figure.return_value = mock_fig - mock_ax.title = MagicMock() - mock_ax.xaxis = MagicMock() - mock_ax.yaxis = MagicMock() - mock_ax.xaxis.label = MagicMock() - mock_ax.yaxis.label = MagicMock() - mock_ax.tick_params = MagicMock() - - # Mock matrix data matching expected output - mock_matrix = { - 'neighborhood_enrichment.csv': pd.DataFrame({ - 'TypeA': [1.0, 0.5, 0.3], - 'TypeB': [0.5, 1.0, 0.7], - 'TypeC': [0.3, 0.7, 1.0] - }, index=['TypeA', 'TypeB', 'TypeC']) - } - - mock_spatial.return_value = { - 'Ax': mock_ax, - 'Matrix': {'annotation': mock_matrix} - } - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters - result = run_from_json(self.params, show_plot=False) - self.assertIsInstance(result, dict) - # Verify both PNG and CSV files were saved - self.assertTrue(len(result) >= 2) # At least 1 PNG + 1 CSV - png_files = [f for f in result.keys() if f.endswith('.png')] - csv_files = [f for f in result.keys() if f.endswith('.csv')] - self.assertEqual(len(png_files), 1) - self.assertEqual(len(csv_files), 1) - - # Test 2: Run without saving - result_no_save = run_from_json( - self.params, save_results=False, show_plot=False - ) - # For multi-plot template, returns None when not saving - self.assertIsNone(result_no_save) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path, show_plot=False) - self.assertIsInstance(result_json, dict) - self.assertTrue(len(result_json) >= 2) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test invalid stratify_by with None not at first position - params_bad = self.params.copy() - params_bad["Stratify_By"] = ["sample", "None", "batch"] - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad, show_plot=False) - - # Check exact error message - expected_msg = ( - 'Found string "None" in the stratify by list that is ' - 'not the first entry.\n' - 'Please remove the "None" to proceed with the list of ' - 'stratify by options, \n' - 'or move the "None" to start of the list to disable ' - 'stratification. Thank you.' - ) - self.assertEqual(str(context.exception), expected_msg) - - @patch('spac.templates.spatial_interaction_template.' - 'spatial_interaction') - def test_function_calls(self, mock_spatial) -> None: - """Test that main function is called with correct parameters.""" - # Mock the spatial_interaction function with stratified output - mock_fig1 = MagicMock() - mock_fig1.number = 1 - mock_fig2 = MagicMock() - mock_fig2.number = 2 - - mock_ax1 = MagicMock() - mock_ax1.get_figure.return_value = mock_fig1 - mock_ax1.title = MagicMock() - mock_ax1.xaxis = MagicMock() - mock_ax1.yaxis = MagicMock() - mock_ax1.xaxis.label = MagicMock() - mock_ax1.yaxis.label = MagicMock() - mock_ax1.tick_params = MagicMock() - - mock_ax2 = MagicMock() - mock_ax2.get_figure.return_value = mock_fig2 - mock_ax2.title = MagicMock() - mock_ax2.xaxis = MagicMock() - mock_ax2.yaxis = MagicMock() - mock_ax2.xaxis.label = MagicMock() - mock_ax2.yaxis.label = MagicMock() - mock_ax2.tick_params = MagicMock() - - # Test with stratification - should produce multiple outputs - mock_spatial.return_value = { - 'Ax': {'sample_S1': mock_ax1, 'sample_S2': mock_ax2}, - 'Matrix': { - 'sample_S1': { - 'S1_enrichment.csv': pd.DataFrame({'A': [1, 2]}) - }, - 'sample_S2': { - 'S2_enrichment.csv': pd.DataFrame({'B': [3, 4]}) - } - } - } - - params_stratified = self.params.copy() - params_stratified["Stratify_By"] = ["sample"] - params_stratified["Seed"] = "42" - params_stratified["Radius"] = "50.0" - params_stratified["Color_Bar_Range"] = "2.5" - params_stratified["Spatial_Analysis_Method"] = ( - "Cluster Interaction Matrix" - ) - - result = run_from_json(params_stratified, show_plot=False) - - # Verify function was called correctly - mock_spatial.assert_called_once() - call_args = mock_spatial.call_args - - # Check specific parameter conversions - self.assertEqual(call_args[1]['annotation'], "cell_type") - self.assertEqual( - call_args[1]['analysis_method'], - "Cluster Interaction Matrix" + def test_spatial_interaction_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run spatial interaction and verify outputs. + + Validates: + 1. saved_files dict has 'figures' and/or 'dataframes' keys + 2. Output files exist and are non-empty + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + show_plot=False, + output_dir=self.tmp_dir.name, ) - self.assertEqual(call_args[1]['stratify_by'], ["sample"]) - self.assertEqual(call_args[1]['seed'], 42) - self.assertEqual(call_args[1]['radius'], 50.0) - self.assertEqual(call_args[1]['n_neighs'], 6) - self.assertEqual(call_args[1]['vmin'], -2.5) - self.assertEqual(call_args[1]['vmax'], 2.5) - self.assertEqual(call_args[1]['cmap'], "seismic") - - # Verify multiple files were saved (2 plots + 2 matrices) - self.assertIsInstance(result, dict) - self.assertEqual(len(result), 4) - png_files = [f for f in result.keys() if f.endswith('.png')] - csv_files = [f for f in result.keys() if f.endswith('.csv')] - self.assertEqual(len(png_files), 2) - self.assertEqual(len(csv_files), 2) + self.assertIsInstance(saved_files, dict) + self.assertGreater(len(saved_files), 0) + for key in ["figures", "dataframes"]: + if key in saved_files: + paths = saved_files[key] + self.assertGreaterEqual(len(paths), 1) + for p in paths: + pf = Path(p) + self.assertTrue(pf.exists()) + self.assertGreater(pf.stat().st_size, 0) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_spatial_plot_template.py b/tests/templates/test_spatial_plot_template.py index 72d6c2b8..2373f894 100644 --- a/tests/templates/test_spatial_plot_template.py +++ b/tests/templates/test_spatial_plot_template.py @@ -1,5 +1,10 @@ # tests/templates/test_spatial_plot_template.py -"""Unit tests for the Spatial Plot template.""" +""" +Real (non-mocked) unit test for the Spatial Plot template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,16 +12,14 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock, call +from pathlib import Path import matplotlib -matplotlib.use("Agg") # Headless backend for CI +matplotlib.use("Agg") import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -25,246 +28,80 @@ from spac.templates.spatial_plot_template import run_from_json -def mock_adata_spatial(n_cells: int = 100) -> ad.AnnData: - """Return a minimal synthetic AnnData with spatial coordinates.""" - rng = np.random.default_rng(0) - - # Create expression matrix - n_features = 5 - X = rng.normal(size=(n_cells, n_features)) - - # Create annotations +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 8 cells with spatial coords for plotting.""" + rng = np.random.default_rng(42) + X = rng.random((8, 2)) obs = pd.DataFrame({ - "cell_type": rng.choice(["TypeA", "TypeB", "TypeC"], n_cells), - "region": rng.choice(["Region1", "Region2"], n_cells), - "slide": rng.choice(["Slide1", "Slide2"], n_cells) + "cell_type": ["A", "B", "A", "B", "A", "B", "A", "B"], }) - - # Create spatial coordinates - spatial_coords = rng.random((n_cells, 2)) * 1000 - - # Create AnnData object - adata = ad.AnnData(X=X, obs=obs) - adata.obsm["spatial"] = spatial_coords - - # Add feature names - adata.var_names = [f"Feature_{i}" for i in range(n_features)] - + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + spatial = rng.random((8, 2)) * 100 + adata = ad.AnnData(X=X, obs=obs, var=var) + adata.obsm["spatial"] = spatial return adata class TestSpatialPlotTemplate(unittest.TestCase): - """Unit tests for the Spatial Plot template.""" + """Real (non-mocked) tests for the spatial plot template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "spatial_data.pickle" - ) - self.out_file = "spatial_plot" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data with spatial info - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata_spatial(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters - self.params = { + params = { "Upstream_Analysis": self.in_file, - "Annotation_to_Highlight": "cell_type", - "Feature_to_Highlight": "", - "Table": "Original", - "Dot_Transparency": 0.5, - "Dot_Size": 25, - "Figure_Height": 6, - "Figure_Width": 12, - "Figure_DPI": 200, - "Font_Size": 12, - "Lower_Colorbar_Bound": 999, - "Upper_Colorbar_Bound": -999, "Color_By": "Annotation", + "Annotation_to_Highlight": "cell_type", + "Feature_to_Highlight": "None", "Stratify": False, "Stratify_By": [], - "Output_File": self.out_file, + "Figure_Width": 6, + "Figure_Height": 4, + "Figure_DPI": 72, + "Font_Size": 10, + "Dot_Size": 50, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "figures": {"type": "directory", "name": "figures_dir"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - @patch('spac.templates.spatial_plot_template.spatial_plot') - @patch('matplotlib.pyplot.show') - def test_run_single_plot_annotation(self, mock_show, mock_spatial) -> None: - """Test single plot with annotation coloring.""" - # Mock the spatial_plot function to return axes - mock_ax = MagicMock() - mock_spatial.return_value = [mock_ax] - - # Test with annotation coloring - saved_files = run_from_json(self.params) - - # Verify spatial_plot was called correctly - mock_spatial.assert_called_once() - call_kwargs = mock_spatial.call_args[1] - self.assertEqual(call_kwargs['annotation'], 'cell_type') - self.assertIsNone(call_kwargs['feature']) - # layer should be None because text_to_value(layer, "Original") - # converts "Original" to None - self.assertIsNone(call_kwargs['layer']) - self.assertEqual(call_kwargs['spot_size'], 25) - self.assertEqual(call_kwargs['alpha'], 0.5) - - # Check saved files - self.assertIn(f"{self.out_file}.png", saved_files) - - # Verify show was called - mock_show.assert_called_once() - - @patch('spac.templates.spatial_plot_template.spatial_plot') - @patch('matplotlib.pyplot.show') - def test_run_single_plot_feature(self, mock_show, mock_spatial) -> None: - """Test single plot with feature coloring.""" - # Mock the spatial_plot function - mock_ax = MagicMock() - mock_spatial.return_value = [mock_ax] - - # Update params for feature coloring - params_feature = self.params.copy() - params_feature["Color_By"] = "Feature" - params_feature["Feature_to_Highlight"] = "Feature_0" - - result = run_from_json(params_feature) - - # Verify spatial_plot was called with feature - call_kwargs = mock_spatial.call_args[1] - self.assertIsNone(call_kwargs['annotation']) - self.assertEqual(call_kwargs['feature'], 'Feature_0') - - @patch('spac.templates.spatial_plot_template.spatial_plot') - @patch('spac.templates.spatial_plot_template.select_values') - @patch('matplotlib.pyplot.show') - @patch('builtins.print') - def test_run_stratified_plots( - self, mock_print, mock_show, mock_select, mock_spatial - ) -> None: - """Test stratified plots generation.""" - # Mock functions - mock_ax = MagicMock() - mock_spatial.return_value = [mock_ax] - - # Mock select_values to return filtered data - mock_adata = mock_adata_spatial(50) - mock_select.return_value = mock_adata - - # Update params for stratification - params_strat = self.params.copy() - params_strat["Stratify"] = True - params_strat["Stratify_By"] = ["region", "slide"] - - saved_files = run_from_json(params_strat) - - # Should save multiple files - self.assertIsInstance(saved_files, dict) - self.assertTrue(len(saved_files) > 1) - - # Verify unique values were printed - print_calls = [str(call[0][0]) for call in mock_print.call_args_list] - # Should print unique values array - self.assertTrue(any('Region' in str(call) for call in print_calls)) - - def test_stratify_validation_error(self) -> None: - """Test error when stratify is True but no stratify_by provided.""" - params_bad = self.params.copy() - params_bad["Stratify"] = True - params_bad["Stratify_By"] = [] - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - expected_msg = ( - 'Please set at least one annotation in the "Stratify By" ' - 'option, or set the "Stratify" to False.' + def test_spatial_plot_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run spatial plot and verify outputs. + + Validates: + 1. saved_files dict has 'figures' key + 2. Figures directory contains non-empty PNG(s) + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + show_plots=False, + output_dir=self.tmp_dir.name, ) - self.assertEqual(str(context.exception), expected_msg) - - @patch('spac.templates.spatial_plot_template.spatial_plot') - @patch('matplotlib.pyplot.show') - def test_run_without_save(self, mock_show, mock_spatial) -> None: - """Test running without saving files.""" - # Mock the spatial_plot function - mock_ax = MagicMock() - mock_spatial.return_value = [mock_ax] - - # Run with save_results=False - result = run_from_json(self.params, save_results=False) - - # Should return None when not saving - self.assertIsNone(result) - @patch('spac.templates.spatial_plot_template.spatial_plot') - @patch('matplotlib.pyplot.show') - def test_max_plots_warning(self, mock_show, mock_spatial) -> None: - """Test warning when too many unique stratification values.""" - # Create adata with many unique combinations - adata = mock_adata_spatial(100) - # Add a column with many unique values - adata.obs['many_values'] = [f'Val_{i}' for i in range(100)] - - with open(self.in_file, 'wb') as f: - pickle.dump(adata, f) - - # Mock functions - mock_ax = MagicMock() - mock_spatial.return_value = [mock_ax] - - params_many = self.params.copy() - params_many["Stratify"] = True - params_many["Stratify_By"] = ["many_values"] - - with patch('builtins.print') as mock_print: - saved_files = run_from_json(params_many) - - # Check warning was printed - print_calls = [ - str(call[0][0]) for call in mock_print.call_args_list - ] - warning_printed = any( - 'displaying only the first 20 plots' in call - for call in print_calls - ) - self.assertTrue(warning_printed) - - def test_json_file_input(self) -> None: - """Test with JSON file input.""" - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - with patch('spac.templates.spatial_plot_template.spatial_plot'): - with patch('matplotlib.pyplot.show'): - result = run_from_json(json_path, save_results=False) - - # Should return None when save_results=False - self.assertIsNone(result) + self.assertIsInstance(saved_files, dict) + self.assertIn("figures", saved_files) - @patch('spac.templates.spatial_plot_template.spatial_plot') - def test_text_to_value_processing(self, mock_spatial) -> None: - """Test that text_to_value is applied to string parameters.""" - # Mock spatial_plot - mock_ax = MagicMock() - mock_spatial.return_value = [mock_ax] - - # Test with "None" strings - params_none = self.params.copy() - params_none["Annotation_to_Highlight"] = "None" - params_none["Color_By"] = "Feature" - params_none["Feature_to_Highlight"] = "Feature_0" - - with patch('matplotlib.pyplot.show'): - run_from_json(params_none, save_results=False) - - # Annotation should be None after text_to_value - call_kwargs = mock_spatial.call_args[1] - self.assertIsNone(call_kwargs['annotation']) + figure_paths = saved_files["figures"] + self.assertGreaterEqual(len(figure_paths), 1) + for fig_path in figure_paths: + fig_file = Path(fig_path) + self.assertTrue(fig_file.exists()) + self.assertGreater(fig_file.stat().st_size, 0) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_subset_analysis_template.py b/tests/templates/test_subset_analysis_template.py index b4e5db38..6d601c4a 100644 --- a/tests/templates/test_subset_analysis_template.py +++ b/tests/templates/test_subset_analysis_template.py @@ -1,5 +1,10 @@ # tests/templates/test_subset_analysis_template.py -"""Unit tests for the Subset Analysis template.""" +""" +Real (non-mocked) unit test for the Subset Analysis template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,13 +12,11 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -22,127 +25,79 @@ from spac.templates.subset_analysis_template import run_from_json -def mock_adata(n_cells: int = 10) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) - - # Create cell labels that match the example - cell_labels = ( - ["Normal_Cells", "Cancer_Cells"] * ((n_cells + 1) // 2) - )[:n_cells] - +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 6 cells, 3 cell types for subset filtering.""" + rng = np.random.default_rng(42) + X = rng.random((6, 2)) obs = pd.DataFrame({ - "cell_labels": cell_labels, - "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] + "cell_type": ["A", "B", "C", "A", "B", "C"], }) - - x_mat = rng.normal(size=(n_cells, 3)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = ["Gene1", "Gene2", "Gene3"] - return adata + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + return ad.AnnData(X=X, obs=obs, var=var) class TestSubsetAnalysisTemplate(unittest.TestCase): - """Unit tests for the Subset Analysis template.""" + """Real (non-mocked) tests for the subset analysis template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "output" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters from JSON template - self.params = { + params = { "Upstream_Analysis": self.in_file, - "Annotation_of_interest": "cell_labels", - "Labels": ["Normal_Cells", "Cancer_Cells"], - "Include_Exclude": "Exclude Selected Labels", - "Output_File": self.out_file, + "Annotation_of_interest": "cell_type", + "Labels": ["A", "B"], + "Output_Directory": self.tmp_dir.name, + "outputs": { + "analysis": {"type": "file", "name": "transform_output.pickle"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with exclude parameters (default from setup) - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - self.assertTrue(len(result) > 0) - - # Test 2: Run without saving - result_no_save = run_from_json(self.params, save_results=False) - # Check appropriate return type based on template - self.assertIsInstance(result_no_save, ad.AnnData) - # Verify that cells were actually filtered - original_adata = mock_adata() - self.assertLess(len(result_no_save), len(original_adata)) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test with non-existent annotation - params_bad = self.params.copy() - params_bad["Annotation_of_interest"] = "non_existent_annotation" - - # This should raise an error when select_values tries to access - # the annotation - with self.assertRaises((KeyError, ValueError)): - run_from_json(params_bad) - - @patch('spac.templates.subset_analysis_template.select_values') - def test_function_calls(self, mock_select) -> None: - """Test that main function is called with correct parameters.""" - # Mock the select_values function to return filtered data - filtered_adata = mock_adata(5) # Smaller filtered dataset - mock_select.return_value = filtered_adata - - # Test with "Include Selected Labels" - params_include = self.params.copy() - params_include["Include_Exclude"] = "Include Selected Labels" - - run_from_json(params_include, save_results=False) - - # Verify function was called correctly - mock_select.assert_called_once() - call_args = mock_select.call_args - - # Check that include mode sets values correctly - self.assertEqual(call_args[1]['annotation'], "cell_labels") - self.assertEqual( - call_args[1]['values'], ["Normal_Cells", "Cancer_Cells"] + def test_subset_analysis_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run subset analysis and verify outputs. + + Validates: + 1. saved_files dict has 'analysis' key + 2. Pickle exists, is non-empty, contains AnnData + 3. Subset has fewer cells than original (only A and B) + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) - self.assertIsNone(call_args[1]['exclude_values']) - - # Reset mock - mock_select.reset_mock() - - # Test with "Exclude Selected Labels" - run_from_json(self.params, save_results=False) - - # Check that exclude mode sets values correctly - call_args = mock_select.call_args - self.assertIsNone(call_args[1]['values']) + + self.assertIsInstance(saved_files, dict) + self.assertIn("analysis", saved_files) + + pkl_path = Path(saved_files["analysis"]) + self.assertTrue(pkl_path.exists()) + self.assertGreater(pkl_path.stat().st_size, 0) + + with open(pkl_path, "rb") as f: + result_adata = pickle.load(f) + self.assertIsInstance(result_adata, ad.AnnData) + # 6 original cells, selecting A and B = 4 cells + self.assertEqual(result_adata.n_obs, 4) self.assertEqual( - call_args[1]['exclude_values'], - ["Normal_Cells", "Cancer_Cells"] + set(result_adata.obs["cell_type"].unique()), {"A", "B"} ) + mem_adata = run_from_json(self.json_file, save_to_disk=False) + self.assertIsInstance(mem_adata, ad.AnnData) + self.assertEqual(mem_adata.n_obs, 4) + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_summarize_annotation_statistics_template.py b/tests/templates/test_summarize_annotation_statistics_template.py index 97904a52..7454a2ff 100644 --- a/tests/templates/test_summarize_annotation_statistics_template.py +++ b/tests/templates/test_summarize_annotation_statistics_template.py @@ -1,5 +1,10 @@ # tests/templates/test_summarize_annotation_statistics_template.py -"""Unit tests for the Summarize Annotation's Statistics template.""" +""" +Real (non-mocked) unit test for the Summarize Annotation Statistics template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,170 +12,86 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" ) from spac.templates.summarize_annotation_statistics_template import ( - run_from_json + run_from_json, ) -def mock_adata(n_cells: int = 10) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 6 cells with cell_type annotation for statistics.""" + rng = np.random.default_rng(42) + X = rng.random((6, 2)) obs = pd.DataFrame({ - "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], - "cluster": ([f"C{i % 3}" for i in range(n_cells)])[:n_cells] + "cell_type": ["A", "A", "B", "B", "B", "C"], }) - x_mat = rng.normal(size=(n_cells, 5)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = [f"Gene{i}" for i in range(5)] - # Add a layer for testing - adata.layers["normalized"] = x_mat * 2.0 - return adata + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + return ad.AnnData(X=X, obs=obs, var=var) class TestSummarizeAnnotationStatisticsTemplate(unittest.TestCase): - """Unit tests for the Summarize Annotation's Statistics template.""" + """Real (non-mocked) tests for summarize annotation statistics.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "annotation_summaries.csv" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters from NIDAP template - self.params = { + params = { "Upstream_Analysis": self.in_file, - "Table_to_Process": "Original", - "Feature_s_": ["All"], "Annotation": "cell_type", - "Output_File": self.out_file, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "dataframe": {"type": "file", "name": "dataframe.csv"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - # Verify CSV file was saved - self.assertIn(self.out_file, result) - csv_path = result[self.out_file] - self.assertTrue(os.path.exists(csv_path)) - - # Verify CSV content structure - df_saved = pd.read_csv(csv_path) - # Should have renamed columns (no spaces/hyphens) - for col in df_saved.columns: - self.assertNotIn(" ", col) - self.assertNotIn("-", col) - - # Test 2: Run without saving - result_no_save = run_from_json(self.params, save_results=False) - # Check appropriate return type - self.assertIsInstance(result_no_save, pd.DataFrame) - self.assertGreater(len(result_no_save), 0) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - @patch('spac.templates.summarize_annotation_statistics_template.' - 'get_cluster_info') - def test_function_calls(self, mock_get_cluster_info) -> None: - """Test that main function is called with correct parameters.""" - # Mock the main function to return a simple DataFrame - mock_df = pd.DataFrame({ - "cluster": ["TypeA", "TypeB"], - "cell count": [5, 5], - "mean-expression": [1.0, 2.0] - }) - mock_get_cluster_info.return_value = mock_df - - # Test with different parameter combinations - - # Test 1: Default parameters - run_from_json(self.params, save_results=False) - mock_get_cluster_info.assert_called_once_with( - adata=mock_get_cluster_info.call_args[1]['adata'], - layer=None, # "Original" → None - annotation="cell_type", - features=None # ["All"] → None - ) - - # Test 2: With specific features and layer - mock_get_cluster_info.reset_mock() - params_features = self.params.copy() - params_features["Feature_s_"] = ["Gene1", "Gene2"] - params_features["Table_to_Process"] = "normalized" - params_features["Annotation"] = "cluster" - - run_from_json(params_features, save_results=False) - mock_get_cluster_info.assert_called_once_with( - adata=mock_get_cluster_info.call_args[1]['adata'], - layer="normalized", - annotation="cluster", - features=["Gene1", "Gene2"] - ) - - # Test 3: With "None" annotation - mock_get_cluster_info.reset_mock() - params_none = self.params.copy() - params_none["Annotation"] = "None" - - run_from_json(params_none, save_results=False) - mock_get_cluster_info.assert_called_once_with( - adata=mock_get_cluster_info.call_args[1]['adata'], - layer=None, - annotation=None, # "None" → None - features=None + def test_summarize_annotation_stats_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: summarize annotation stats and verify outputs. + + Validates: + 1. saved_files dict has 'dataframe' key + 2. CSV exists and is non-empty + 3. Summary includes count/percentage information + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) - def test_column_renaming(self) -> None: - """Test that columns with spaces and hyphens are renamed.""" - with patch('spac.templates.summarize_annotation_statistics_template.' - 'get_cluster_info') as mock_func: - # Create a DataFrame with problematic column names - mock_df = pd.DataFrame({ - "cell type": ["A", "B"], - "mean-expression": [1.0, 2.0], - "std dev": [0.1, 0.2], - "CD4+ count": [10, 20] - }) - mock_func.return_value = mock_df - - # Run without saving to get the DataFrame directly - result_df = run_from_json(self.params, save_results=False) - - # Check that columns are renamed - expected_columns = ["cell_type", "mean_expression", "std_dev", - "CD4+_count"] - self.assertEqual(list(result_df.columns), expected_columns) + self.assertIsInstance(saved_files, dict) + self.assertIn("dataframe", saved_files) + + csv_path = Path(saved_files["dataframe"]) + self.assertTrue(csv_path.exists()) + self.assertGreater(csv_path.stat().st_size, 0) + + result_df = pd.read_csv(csv_path) + self.assertGreater(len(result_df), 0) + + mem_df = run_from_json(self.json_file, save_to_disk=False) + self.assertIsInstance(mem_df, pd.DataFrame) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_summarize_dataframe_template.py b/tests/templates/test_summarize_dataframe_template.py index c179eaa0..516a3053 100644 --- a/tests/templates/test_summarize_dataframe_template.py +++ b/tests/templates/test_summarize_dataframe_template.py @@ -1,18 +1,19 @@ # tests/templates/test_summarize_dataframe_template.py -"""Unit tests for the Summarize DataFrame template.""" +""" +Real (non-mocked) unit test for the Summarize DataFrame template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os -import pickle import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path -import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -21,233 +22,64 @@ from spac.templates.summarize_dataframe_template import run_from_json -def mock_dataframe(n_rows: int = 10) -> pd.DataFrame: - """Return a minimal synthetic DataFrame for fast tests.""" - rng = np.random.default_rng(0) - df = pd.DataFrame({ - "file_name": [f"file_{i}.csv" for i in range(n_rows)], - "CD3D": rng.random(n_rows) * 100, - "FOXP3": rng.random(n_rows) * 50, - "PDL1": rng.random(n_rows) * 75, +def _make_tiny_dataframe() -> pd.DataFrame: + """Minimal synthetic DataFrame for summarization.""" + return pd.DataFrame({ + "cell_type": ["A", "B", "A", "B", "C", "C"], + "marker_1": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0], + "marker_2": [10.0, 20.0, 30.0, 40.0, 50.0, 60.0], }) - # Add some NaN values for testing - df.loc[2, "CD3D"] = np.nan - df.loc[5, "FOXP3"] = np.nan - return df class TestSummarizeDataFrameTemplate(unittest.TestCase): - """Unit tests for the Summarize DataFrame template.""" + """Real (non-mocked) tests for the summarize dataframe template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.csv" - ) - self.out_file = "summary" + self.in_file = os.path.join(self.tmp_dir.name, "input.csv") - # Save minimal mock data - mock_dataframe().to_csv(self.in_file, index=False) + _make_tiny_dataframe().to_csv(self.in_file, index=False) - # Minimal parameters - from the example - self.params = { - "Calculate_Centroids": self.in_file, - "Columns": ["file_name", "CD3D", "FOXP3", "PDL1"], - "Print_Missing_Location": True, - "Output_File": self.out_file, + params = { + "Upstream_Dataset": self.in_file, + "Columns": ["cell_type", "marker_1", "marker_2"], + "Output_Directory": self.tmp_dir.name, + "outputs": { + "html": {"type": "directory", "name": "html_dir"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters - with patch('spac.templates.summarize_dataframe_template.' - 'summarize_dataframe') as mock_summarize: - with patch('spac.templates.summarize_dataframe_template.' - 'present_summary_as_figure') as mock_fig: - # Mock the summary dataframe - mock_summarize.return_value = pd.DataFrame( - {"test": [1, 2, 3]} - ) - - # Mock the Plotly figure - mock_plotly_fig = MagicMock() - mock_plotly_fig.write_html = MagicMock() - mock_plotly_fig.show = MagicMock() - mock_fig.return_value = mock_plotly_fig - - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - self.assertIn("summary.html", result) - - # Verify figure methods were called - mock_plotly_fig.show.assert_called_once() - mock_plotly_fig.write_html.assert_called_once_with( - "summary.html" - ) - - # Test 2: Run without saving - with patch('spac.templates.summarize_dataframe_template.' - 'summarize_dataframe') as mock_summarize: - with patch('spac.templates.summarize_dataframe_template.' - 'present_summary_as_figure') as mock_fig: - mock_summarize.return_value = pd.DataFrame( - {"test": [1, 2, 3]} - ) - mock_plotly_fig = MagicMock() - mock_fig.return_value = mock_plotly_fig - - result_no_save = run_from_json( - self.params, save_results=False - ) - # Should return tuple of (figure, dataframe) - self.assertIsInstance(result_no_save, tuple) - self.assertEqual(len(result_no_save), 2) - fig, summary = result_no_save - self.assertEqual(fig, mock_plotly_fig) - self.assertIsInstance(summary, pd.DataFrame) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - with patch('spac.templates.summarize_dataframe_template.' - 'summarize_dataframe') as mock_summarize: - with patch('spac.templates.summarize_dataframe_template.' - 'present_summary_as_figure') as mock_fig: - mock_summarize.return_value = pd.DataFrame( - {"test": [1, 2, 3]} - ) - mock_plotly_fig = MagicMock() - mock_plotly_fig.write_html = MagicMock() - mock_plotly_fig.show = MagicMock() - mock_fig.return_value = mock_plotly_fig - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - @patch('spac.templates.summarize_dataframe_template.' - 'summarize_dataframe') - @patch('spac.templates.summarize_dataframe_template.' - 'present_summary_as_figure') - def test_function_calls(self, mock_present, mock_summarize) -> None: - """Test that main functions are called with correct parameters.""" - # Mock the functions - mock_summary = pd.DataFrame({"test": [1, 2, 3]}) - mock_summarize.return_value = mock_summary - - mock_fig = MagicMock() - mock_fig.write_html = MagicMock() - mock_fig.show = MagicMock() - mock_present.return_value = mock_fig - - run_from_json(self.params) - - # Verify summarize_dataframe was called correctly - mock_summarize.assert_called_once() - call_args = mock_summarize.call_args - - # Check the dataframe was passed - df_arg = call_args[0][0] - self.assertIsInstance(df_arg, pd.DataFrame) - self.assertEqual(len(df_arg), 10) - - # Check keyword arguments - self.assertEqual( - call_args[1]['columns'], - ["file_name", "CD3D", "FOXP3", "PDL1"] - ) - self.assertEqual(call_args[1]['print_nan_locations'], True) - - # Verify present_summary_as_figure was called - mock_present.assert_called_once_with(mock_summary) - - def test_pickle_input(self) -> None: - """Test loading from pickle file.""" - pickle_file = os.path.join(self.tmp_dir.name, "input.pickle") - with open(pickle_file, 'wb') as f: - pickle.dump(mock_dataframe(), f) - - params_pickle = self.params.copy() - params_pickle["Calculate_Centroids"] = pickle_file - - with patch('spac.templates.summarize_dataframe_template.' - 'summarize_dataframe') as mock_summarize: - with patch('spac.templates.summarize_dataframe_template.' - 'present_summary_as_figure') as mock_fig: - mock_summarize.return_value = pd.DataFrame( - {"test": [1, 2, 3]} - ) - mock_plotly_fig = MagicMock() - mock_plotly_fig.write_html = MagicMock() - mock_plotly_fig.show = MagicMock() - mock_fig.return_value = mock_plotly_fig - - result = run_from_json(params_pickle) - self.assertIsInstance(result, dict) - - def test_optional_parameter_defaults(self) -> None: - """Test that optional parameters use correct defaults.""" - params_minimal = { - "Calculate_Centroids": self.in_file, - "Columns": ["file_name", "CD3D"] - # Print_Missing_Location not specified - } - - with patch('spac.templates.summarize_dataframe_template.' - 'summarize_dataframe') as mock_summarize: - with patch('spac.templates.summarize_dataframe_template.' - 'present_summary_as_figure') as mock_fig: - mock_summarize.return_value = pd.DataFrame( - {"test": [1, 2, 3]} - ) - mock_plotly_fig = MagicMock() - mock_plotly_fig.show = MagicMock() - mock_fig.return_value = mock_plotly_fig - - run_from_json(params_minimal, save_results=False) - - # Check default value for Print_Missing_Location - mock_summarize.assert_called_once() - call_args = mock_summarize.call_args - self.assertEqual( - call_args[1]['print_nan_locations'], - False # Default from template JSON - ) - - @patch('builtins.print') - @patch('spac.templates.summarize_dataframe_template.' - 'summarize_dataframe') - def test_console_output(self, mock_summarize, mock_print) -> None: - """Test that NaN locations are printed when requested.""" - # Create summary with NaN info - mock_summary = pd.DataFrame({ - 'column': ['CD3D', 'FOXP3'], - 'missing_indices': [[2], [5]] - }) - mock_summarize.return_value = mock_summary - - with patch('spac.templates.summarize_dataframe_template.' - 'present_summary_as_figure') as mock_fig: - mock_plotly_fig = MagicMock() - mock_plotly_fig.show = MagicMock() - mock_fig.return_value = mock_plotly_fig - - run_from_json(self.params, save_results=False) - - # The summarize_dataframe function itself handles printing - # We just verify it was called with print_nan_locations=True - mock_summarize.assert_called_once() - self.assertTrue( - mock_summarize.call_args[1]['print_nan_locations'] + def test_summarize_dataframe_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: summarize dataframe and verify outputs. + + Validates: + 1. saved_files dict has 'html' key + 2. HTML directory contains non-empty file(s) + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) + self.assertIsInstance(saved_files, dict) + self.assertIn("html", saved_files) + + html_paths = saved_files["html"] + self.assertGreaterEqual(len(html_paths), 1) + for html_path in html_paths: + html_file = Path(html_path) + self.assertTrue(html_file.exists()) + self.assertGreater(html_file.stat().st_size, 0) + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_template_utils.py b/tests/templates/test_template_utils.py index b8548bb1..76cd2a58 100644 --- a/tests/templates/test_template_utils.py +++ b/tests/templates/test_template_utils.py @@ -1,5 +1,14 @@ # tests/templates/test_template_utils.py -"""Unit tests for template utilities.""" +""" +Real (non-mocked) unit tests for template utility functions. + +Validates utility I/O behaviour only: + • Functions produce correct outputs from real inputs + • File I/O operations work on real filesystem + • Error messages are accurate + +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -8,11 +17,11 @@ import tempfile import unittest import warnings -from unittest.mock import patch import anndata as ad import numpy as np import pandas as pd from pathlib import Path +import matplotlib.pyplot as plt sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -20,16 +29,20 @@ from spac.templates.template_utils import ( load_input, - save_outputs, + save_results, + _save_single_object, text_to_value, convert_pickle_to_h5ad, convert_to_floats, spell_out_special_characters, - load_csv_files + load_csv_files, + parse_params, + string_list_to_dictionary, + clean_column_name, ) -def mock_adata(n_cells: int = 10) -> ad.AnnData: +def create_test_adata(n_cells: int = 10) -> ad.AnnData: """Return a minimal synthetic AnnData for fast tests.""" rng = np.random.default_rng(0) obs = pd.DataFrame({ @@ -40,7 +53,7 @@ def mock_adata(n_cells: int = 10) -> ad.AnnData: return adata -def mock_dataframe(n_rows: int = 5) -> pd.DataFrame: +def create_test_dataframe(n_rows: int = 5) -> pd.DataFrame: """Return a minimal DataFrame for fast tests.""" return pd.DataFrame({ "col1": range(n_rows), @@ -53,8 +66,8 @@ class TestTemplateUtils(unittest.TestCase): def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.test_adata = mock_adata() - self.test_df = mock_dataframe() + self.test_adata = create_test_adata() + self.test_df = create_test_dataframe() def tearDown(self) -> None: self.tmp_dir.cleanup() @@ -93,29 +106,7 @@ def test_complete_io_workflow(self) -> None: loaded_p = load_input(p_path) self.assertEqual(loaded_p.n_obs, 10) - # Test 5: Save outputs - multiple formats - outputs = { - "result.pickle": self.test_adata, # Now preferred format - "data.csv": self.test_df, - "adata.pkl": self.test_adata, - "adata.h5ad": self.test_adata, # Still supported - "other_data": {"key": "value"} # Defaults to pickle - } - saved_files = save_outputs(outputs, self.tmp_dir.name) - - # Verify all files were saved - self.assertEqual(len(saved_files), 5) - for filename, filepath in saved_files.items(): - self.assertTrue(os.path.exists(filepath)) - self.assertIn(filename, saved_files) - - # Verify CSV content - csv_path = saved_files["data.csv"] - loaded_df = pd.read_csv(csv_path) - self.assertEqual(len(loaded_df), 5) - self.assertIn("col1", loaded_df.columns) - - # Test 6: Convert pickle to h5ad + # Test 5: Convert pickle to h5ad pickle_src = os.path.join( self.tmp_dir.name, "convert_src.pickle" ) @@ -282,18 +273,6 @@ def test_convert_pickle_to_h5ad_wrong_type_error_message(self) -> None: actual_msg = str(context.exception) self.assertEqual(expected_msg, actual_msg) - def test_default_pickle_extension(self) -> None: - """Test that files without extension default to pickle.""" - outputs = { - "no_extension": self.test_adata - } - saved_files = save_outputs(outputs, self.tmp_dir.name) - - # Should have .pickle extension - filepath = saved_files["no_extension"] - self.assertTrue(filepath.endswith('.pickle')) - self.assertTrue(os.path.exists(filepath)) - def test_spell_out_special_characters(self) -> None: """Test spell_out_special_characters function.""" from spac.templates.template_utils import spell_out_special_characters @@ -448,7 +427,7 @@ def test_load_csv_files(self) -> None: 'file_name': ['missing.csv'], 'experiment': ['Exp3'] }) - with self.assertRaises(TypeError) as context: + with self.assertRaises(FileNotFoundError) as context: load_csv_files(csv_dir, config_missing) self.assertIn("not found", str(context.exception)) @@ -459,33 +438,30 @@ def test_load_csv_files(self) -> None: 'file_name': ['empty.csv'], 'experiment': ['Exp4'] }) - with self.assertRaises(TypeError) as context: + with self.assertRaises(ValueError) as context: load_csv_files(csv_dir, config_empty) self.assertIn("empty", str(context.exception)) - # Test 7: First file validation for string_columns + # Test 7: Non-existent string_columns are silently ignored config_single = pd.DataFrame({ 'file_name': ['data1.csv'] }) - with self.assertRaises(ValueError): - # Non-existent column should raise error - load_csv_files( - csv_dir, config_single, - string_columns=['NonExistentColumn'] - ) - - @patch('builtins.print') - def test_load_csv_files_console_output(self, mock_print) -> None: - """Test console output from load_csv_files.""" - from spac.templates.template_utils import load_csv_files + result_nonexist = load_csv_files( + csv_dir, config_single, + string_columns=['NonExistentColumn'] + ) + self.assertIsInstance(result_nonexist, pd.DataFrame) - # Setup test data + def test_load_csv_files_special_character_column_cleaning(self) -> None: + """Test that load_csv_files cleans special character column names.""" + # Setup test data with special character columns csv_dir = Path(self.tmp_dir.name) / "csv_test" csv_dir.mkdir() csv_data = pd.DataFrame({ 'ID': [1, 2], - 'CD4+': ['pos', 'neg'] # Special character + 'CD4+': ['pos', 'neg'], # Special character + 'Area µm²': [100.0, 200.0], }) csv_data.to_csv(csv_dir / 'test.csv', index=False) @@ -494,30 +470,520 @@ def test_load_csv_files_console_output(self, mock_print) -> None: 'group': ['A'] }) - # Run function - load_csv_files(csv_dir, config) + result = load_csv_files(csv_dir, config) + + # Assert: special character columns cleaned + self.assertIn('CD4_pos', result.columns) + self.assertIn('Area_um2', result.columns) + self.assertNotIn('CD4+', result.columns) + self.assertNotIn('Area µm²', result.columns) + + # Assert: data integrity preserved + self.assertEqual(len(result), 2) + self.assertEqual(result['group'].unique().tolist(), ['A']) + + def test_save_results_single_csv_file(self) -> None: + """Test saving DataFrame as single CSV file using save_results.""" + # Setup + df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]}) + + params = { + "outputs": { + "dataframe": {"type": "file", "name": "data.csv"} + } + } + + results = { + "dataframe": df + } + + # Execute + saved = save_results(results, params, self.tmp_dir.name) + + # Verify + csv_path = Path(self.tmp_dir.name) / "data.csv" + self.assertTrue(csv_path.exists()) + self.assertTrue(csv_path.is_file()) + + # Check content + loaded_df = pd.read_csv(csv_path) + pd.testing.assert_frame_equal(loaded_df, df) + + def test_save_results_multiple_csvs_directory(self) -> None: + """Test saving multiple DataFrames in directory using save_results.""" + # Setup + df1 = pd.DataFrame({'X': [1, 2]}) + df2 = pd.DataFrame({'Y': [3, 4]}) + + params = { + "outputs": { + "dataframe": {"type": "directory", "name": "dataframe_dir"} + } + } + + results = { + "dataframe": { + "first": df1, + "second": df2 + } + } + + # Execute + saved = save_results(results, params, self.tmp_dir.name) + + # Verify + dir_path = Path(self.tmp_dir.name) / "dataframe_dir" + self.assertTrue(dir_path.exists()) + self.assertTrue(dir_path.is_dir()) + self.assertTrue((dir_path / "first.csv").exists()) + self.assertTrue((dir_path / "second.csv").exists()) + + def test_save_results_figures_directory(self) -> None: + """Test saving multiple figures in directory using save_results.""" + # Suppress matplotlib warnings + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Setup + fig1, ax1 = plt.subplots() + ax1.plot([1, 2, 3]) + + fig2, ax2 = plt.subplots() + ax2.bar(['A', 'B'], [5, 10]) + + params = { + "outputs": { + "figures": {"type": "directory", "name": "plots"} + } + } + + results = { + "figures": { + "line_plot": fig1, + "bar_plot": fig2 + } + } + + # Execute + saved = save_results(results, params, self.tmp_dir.name) + + # Verify + plots_dir = Path(self.tmp_dir.name) / "plots" + self.assertTrue(plots_dir.exists()) + self.assertTrue(plots_dir.is_dir()) + self.assertTrue((plots_dir / "line_plot.png").exists()) + self.assertTrue((plots_dir / "bar_plot.png").exists()) + + # Clean up + plt.close('all') + + def test_save_results_analysis_pickle_file(self) -> None: + """Test saving analysis object as pickle file using save_results.""" + # Setup + analysis = { + "method": "test_analysis", + "results": [1, 2, 3, 4, 5], + "params": {"alpha": 0.05} + } + + params = { + "outputs": { + "analysis": {"type": "file", "name": "results.pickle"} + } + } + + results = { + "analysis": analysis + } + + # Execute + saved = save_results(results, params, self.tmp_dir.name) + + # Verify + pickle_path = Path(self.tmp_dir.name) / "results.pickle" + self.assertTrue(pickle_path.exists()) + self.assertTrue(pickle_path.is_file()) + + # Check content + with open(pickle_path, 'rb') as f: + loaded = pickle.load(f) + self.assertEqual(loaded, analysis) + + def test_save_results_html_directory(self) -> None: + """Test saving HTML reports in directory using save_results.""" + # Setup + html1 = "

Report 1

" + html2 = "

Report 2

" + + params = { + "outputs": { + "html": {"type": "directory", "name": "reports"} + } + } + + results = { + "html": { + "main": html1, + "summary": html2 + } + } + + # Execute + saved = save_results(results, params, self.tmp_dir.name) + + # Verify + reports_dir = Path(self.tmp_dir.name) / "reports" + self.assertTrue(reports_dir.exists()) + self.assertTrue(reports_dir.is_dir()) + self.assertTrue((reports_dir / "main.html").exists()) + self.assertTrue((reports_dir / "summary.html").exists()) + + # Check content + with open(reports_dir / "main.html") as f: + content = f.read() + self.assertIn("Report 1", content) + + def test_save_results_complete_configuration(self) -> None: + """Test complete configuration with all output types using save_results.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Setup + fig, ax = plt.subplots() + ax.plot([1, 2, 3]) + + df = pd.DataFrame({'A': [1, 2]}) + analysis = {"result": "complete"} + html = "Report" + + params = { + "outputs": { + "figures": {"type": "directory", "name": "figure_dir"}, + "dataframe": {"type": "file", "name": "dataframe.csv"}, + "analysis": {"type": "file", "name": "output.pickle"}, + "html": {"type": "directory", "name": "html_dir"} + } + } + + results = { + "figures": {"plot": fig}, + "dataframe": df, + "analysis": analysis, + "html": {"report": html} + } + + # Execute + saved = save_results(results, params, self.tmp_dir.name) + + # Verify all outputs created + self.assertTrue((Path(self.tmp_dir.name) / "figure_dir").is_dir()) + self.assertTrue((Path(self.tmp_dir.name) / "dataframe.csv").is_file()) + self.assertTrue((Path(self.tmp_dir.name) / "output.pickle").is_file()) + self.assertTrue((Path(self.tmp_dir.name) / "html_dir").is_dir()) + + # Clean up + plt.close('all') + + def test_save_results_case_insensitive_matching(self) -> None: + """Test case-insensitive matching of result keys to config.""" + # Setup + df = pd.DataFrame({'A': [1, 2]}) + + params = { + "outputs": { + "dataframe": {"type": "file", "name": "data.csv"} # Capital D + } + } + + results = { + "dataframe": df # lowercase d + } + + # Execute + saved = save_results(results, params, self.tmp_dir.name) + + # Should still match and save + self.assertTrue((Path(self.tmp_dir.name) / "data.csv").exists()) + + def test_save_results_missing_config(self) -> None: + """Test that missing config for result type generates warning.""" + # Setup + df = pd.DataFrame({'A': [1, 2]}) + + params = { + "outputs": { + # No config for "dataframes" + "figures": {"type": "directory", "name": "plots"} + } + } + + results = { + "dataframe": df, # No matching config + "figures": {} + } + + # Execute (should not raise, just warn) + saved = save_results(results, params, self.tmp_dir.name) + + # Only figures should be in saved files + self.assertIn("figures", saved) + self.assertNotIn("dataframes", saved) + self.assertNotIn("DataFrames", saved) + + def test_save_single_object_dataframe(self) -> None: + """Test _save_single_object helper with DataFrame.""" + df = pd.DataFrame({'A': [1, 2]}) + + path = _save_single_object(df, "test", Path(self.tmp_dir.name)) + + self.assertEqual(path.name, "test.csv") + self.assertTrue(path.exists()) + + def test_save_single_object_figure(self) -> None: + """Test _save_single_object helper with matplotlib figure.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + fig, ax = plt.subplots() + ax.plot([1, 2, 3]) + + path = _save_single_object(fig, "plot", Path(self.tmp_dir.name)) + + self.assertEqual(path.name, "plot.png") + self.assertTrue(path.exists()) + + plt.close('all') + + def test_save_single_object_html(self) -> None: + """Test _save_single_object helper with HTML string.""" + html = "Test" + + path = _save_single_object(html, "report.html", Path(self.tmp_dir.name)) + + self.assertEqual(path.name, "report.html") + self.assertTrue(path.exists()) + + def test_save_single_object_generic(self) -> None: + """Test _save_single_object helper with generic object.""" + data = {"test": "data", "value": 123} + + path = _save_single_object(data, "data", Path(self.tmp_dir.name)) + + self.assertEqual(path.name, "data.pickle") + self.assertTrue(path.exists()) + + def test_save_results_dataframes_both_configurations(self) -> None: + """Test DataFrames can be saved as both file and directory.""" + # Test 1: Single DataFrame as file + df_single = pd.DataFrame({'A': [1, 2, 3]}) + + params_file = { + "outputs": { + "dataframe": {"type": "file", "name": "single.csv"} + } + } + + results_single = {"dataframe": df_single} + + saved = save_results(results_single, params_file, self.tmp_dir.name) + self.assertTrue((Path(self.tmp_dir.name) / "single.csv").exists()) + + # Test 2: Multiple DataFrames as directory + df1 = pd.DataFrame({'X': [1, 2]}) + df2 = pd.DataFrame({'Y': [3, 4]}) + + params_dir = { + "outputs": { + "dataframe": {"type": "directory", "name": "multi_df"} + } + } + + results_multi = { + "dataframe": { + "data1": df1, + "data2": df2 + } + } + + saved = save_results(results_multi, params_dir, + os.path.join(self.tmp_dir.name, "test2")) + + dir_path = Path(self.tmp_dir.name) / "test2" / "multi_df" + self.assertTrue(dir_path.exists()) + self.assertTrue(dir_path.is_dir()) + self.assertTrue((dir_path / "data1.csv").exists()) + self.assertTrue((dir_path / "data2.csv").exists()) + + def test_save_results_auto_type_detection(self) -> None: + """Test automatic type detection based on standardized schema.""" + # Setup - params with no explicit type + params = { + "outputs": { + "figures": {"name": "plot.png"}, # No type specified + "analysis": {"name": "results.pickle"}, # No type specified + "dataframe": {"name": "data.csv"}, # No type specified + "html": {"name": "report_dir"} # No type specified + } + } + + # Create test data + fig, ax = plt.subplots() + ax.plot([1, 2, 3]) + + results = { + "figures": {"plot1": fig, "plot2": fig}, # Should auto-detect as directory + "analysis": {"data": [1, 2, 3]}, # Should auto-detect as file + "dataframe": pd.DataFrame({'A': [1, 2]}), # Should auto-detect as file + "html": {"report": ""} # Should auto-detect as directory + } + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # Execute + saved = save_results(results, params, self.tmp_dir.name) + + # Verify auto-detection worked correctly + # figure should be directory (standardized for figures) + self.assertTrue((Path(self.tmp_dir.name) / "plot.png").is_dir()) + + # analysis should be file + self.assertTrue((Path(self.tmp_dir.name) / "results.pickle").is_file()) + + # dataframes should be file (standard case) + self.assertTrue((Path(self.tmp_dir.name) / "data.csv").is_file()) + + # html should be directory (standardized for html) + self.assertTrue((Path(self.tmp_dir.name) / "report_dir").is_dir()) + + plt.close('all') + + def test_save_results_neighborhood_profile_special_case(self) -> None: + """Test special case for Neighborhood Profile as directory.""" + # Setup - Neighborhood Profile should be directory even though it's a dataframe + params = { + "outputs": { + "dataframes": {"name": "Neighborhood_Profile_Results"} # No type, should auto-detect + } + } + + df1 = pd.DataFrame({'X': [1, 2]}) + df2 = pd.DataFrame({'Y': [3, 4]}) + + results = { + "dataframes": { + "profile1": df1, + "profile2": df2 + } + } + + # Execute + saved = save_results(results, params, self.tmp_dir.name) + + # Verify it was saved as directory (special case) + dir_path = Path(self.tmp_dir.name) / "Neighborhood_Profile_Results" + self.assertTrue(dir_path.exists()) + self.assertTrue(dir_path.is_dir()) + self.assertTrue((dir_path / "profile1.csv").exists()) + self.assertTrue((dir_path / "profile2.csv").exists()) + + def test_save_results_with_output_directory_param(self) -> None: + """Test using Output_Directory from params.""" + custom_dir = os.path.join(self.tmp_dir.name, "custom_output") + + # Setup - params includes Output_Directory + params = { + "Output_Directory": custom_dir, + "outputs": { + "dataframes": {"type": "file", "name": "data.csv"} + } + } + + results = { + "dataframes": pd.DataFrame({'A': [1, 2]}) + } + + # Execute without specifying output_base_dir (should use params) + saved = save_results(results, params) + + # Verify it used the Output_Directory from params + csv_path = Path(custom_dir) / "data.csv" + self.assertTrue(csv_path.exists()) + + def test_parse_params_from_json_file(self) -> None: + """Test parse_params loads parameters from a JSON file.""" + params = {"key1": "value1", "key2": 42, "nested": {"a": True}} + json_path = os.path.join(self.tmp_dir.name, "params.json") + with open(json_path, "w") as f: + json.dump(params, f) + + result = parse_params(json_path) + + self.assertEqual(result, params) + self.assertEqual(result["key1"], "value1") + self.assertEqual(result["key2"], 42) + self.assertTrue(result["nested"]["a"]) + + def test_parse_params_from_dict(self) -> None: + """Test parse_params passes through a dict unchanged.""" + params = {"key": "value"} + result = parse_params(params) + self.assertIs(result, params) + + def test_parse_params_from_json_string(self) -> None: + """Test parse_params parses a raw JSON string.""" + json_str = '{"key": "value", "num": 7}' + result = parse_params(json_str) + self.assertEqual(result, {"key": "value", "num": 7}) + + def test_parse_params_invalid_type_raises(self) -> None: + """Test parse_params raises TypeError for unsupported input.""" + with self.assertRaises(TypeError): + parse_params(12345) + + def test_string_list_to_dictionary_valid(self) -> None: + """Test string_list_to_dictionary with valid key:value pairs.""" + result = string_list_to_dictionary( + ["red:#FF0000", "blue:#0000FF"] + ) + self.assertEqual(result, {"red": "#FF0000", "blue": "#0000FF"}) + + def test_string_list_to_dictionary_custom_names(self) -> None: + """Test string_list_to_dictionary with custom key/value names.""" + result = string_list_to_dictionary( + ["TypeA:Cancer", "TypeB:Normal"], + key_name="cell_type", + value_name="diagnosis", + ) + self.assertEqual( + result, {"TypeA": "Cancer", "TypeB": "Normal"} + ) + + def test_string_list_to_dictionary_invalid_entry(self) -> None: + """Test string_list_to_dictionary raises on missing colon.""" + with self.assertRaises(ValueError) as ctx: + string_list_to_dictionary(["valid:pair", "no_colon"]) + self.assertIn("Missing ':'", str(ctx.exception)) + + def test_string_list_to_dictionary_not_list_raises(self) -> None: + """Test string_list_to_dictionary raises TypeError for non-list.""" + with self.assertRaises(TypeError): + string_list_to_dictionary("not_a_list") - # Check console output - print_calls = [str(call[0][0]) for call in mock_print.call_args_list - if call[0]] + def test_clean_column_name_basic(self) -> None: + """Test clean_column_name on normal and special-char columns.""" + # Normal name — unchanged + self.assertEqual(clean_column_name("cell_type"), "cell_type") - # Should print column name updates - updates = [msg for msg in print_calls - if 'Column Name Updated:' in msg] - self.assertTrue(len(updates) > 0) - # The function strips trailing underscores, so CD4+ becomes CD4_pos - self.assertTrue(any('CD4+' in msg and 'CD4_pos' in msg - for msg in updates)) + # Special characters cleaned + self.assertEqual(clean_column_name("CD4+"), "CD4_pos") + self.assertEqual(clean_column_name("Area µm²"), "Area_um2") - # Should print processing messages - processing = [msg for msg in print_calls - if 'Processing file:' in msg] - self.assertTrue(len(processing) > 0) + def test_clean_column_name_digit_prefix(self) -> None: + """Test clean_column_name adds col_ prefix for digit-leading names.""" + result = clean_column_name("123ABC") + self.assertEqual(result, "col_123ABC") - # Should print final info - final_info = [msg for msg in print_calls - if 'Final Dataframe Info' in msg] - self.assertTrue(len(final_info) > 0) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_tsne_analysis_template.py b/tests/templates/test_tsne_analysis_template.py index 7694f191..72039f1e 100644 --- a/tests/templates/test_tsne_analysis_template.py +++ b/tests/templates/test_tsne_analysis_template.py @@ -1,5 +1,10 @@ # tests/templates/test_tsne_analysis_template.py -"""Unit tests for the tSNE Analysis template.""" +""" +Real (non-mocked) unit test for the tSNE Analysis template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,13 +12,11 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -22,114 +25,71 @@ from spac.templates.tsne_analysis_template import run_from_json -def mock_adata(n_cells: int = 100) -> ad.AnnData: - """ - Return a minimal synthetic AnnData for fast tests. - - Note: tSNE requires n_cells > perplexity (default 30), - so we use 100 cells by default. - """ - rng = np.random.default_rng(0) - obs = pd.DataFrame({ - "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], - "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] - }) - x_mat = rng.normal(size=(n_cells, 10)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = [f"Gene{i}" for i in range(10)] - # Add spatial coordinates if needed - adata.obsm["spatial"] = rng.random((n_cells, 2)) * 100 - return adata +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 50 cells, 5 genes for tSNE (needs enough cells).""" + rng = np.random.default_rng(42) + X = rng.random((50, 5)) + obs = pd.DataFrame({"cell_type": ["A", "B"] * 25}) + var = pd.DataFrame(index=[f"Gene_{i}" for i in range(5)]) + return ad.AnnData(X=X, obs=obs, var=var) class TestTsneAnalysisTemplate(unittest.TestCase): - """Unit tests for the tSNE Analysis template.""" + """Real (non-mocked) tests for the tSNE analysis template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "output" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(100), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters - adjust based on template - self.params = { + params = { "Upstream_Analysis": self.in_file, "Table_to_Process": "Original", - "Output_File": self.out_file, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - # Verify a pickle file was created - self.assertTrue( - any('.pickle' in str(f) for f in result.values()) - ) - - # Test 2: Run without saving - result_no_save = run_from_json(self.params, save_results=False) - # Check appropriate return type based on template - self.assertIsInstance(result_no_save, ad.AnnData) - # Verify that tSNE was added to obsm - self.assertIn("X_tsne", result_no_save.obsm) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - def test_layer_parameter(self) -> None: - """Test that layer parameter is handled correctly.""" - # Create adata with a layer - use 100 cells for tSNE - adata = mock_adata(100) - adata.layers["normalized"] = adata.X * 2 - - with open(self.in_file, 'wb') as f: - pickle.dump(adata, f) - - # Test with specific layer - params_layer = self.params.copy() - params_layer["Table_to_Process"] = "normalized" - - result = run_from_json(params_layer, save_results=False) - self.assertIn("X_tsne", result.obsm) - - @patch('spac.templates.tsne_analysis_template.tsne') - def test_function_calls(self, mock_tsne) -> None: - """Test that main function is called with correct parameters.""" - # Mock the tsne function - def mock_tsne_func(adata, layer=None): - # Simulate adding tSNE results - adata.obsm["X_tsne"] = np.random.rand(adata.n_obs, 2) - return adata - - mock_tsne.side_effect = mock_tsne_func - - run_from_json(self.params) - - # Verify function was called correctly - mock_tsne.assert_called_once() - call_args = mock_tsne.call_args - - # Check that layer=None when "Original" is specified - self.assertIsNone(call_args[1]['layer']) + def test_tsne_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run tSNE and verify outputs. + + Validates: + 1. saved_files dict has 'analysis' key + 2. Pickle contains AnnData with 'X_tsne' in .obsm + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, + ) + + self.assertIsInstance(saved_files, dict) + self.assertIn("analysis", saved_files) + + pkl_path = Path(saved_files["analysis"]) + self.assertTrue(pkl_path.exists()) + self.assertGreater(pkl_path.stat().st_size, 0) + + with open(pkl_path, "rb") as f: + result_adata = pickle.load(f) + self.assertIsInstance(result_adata, ad.AnnData) + self.assertIn("X_tsne", result_adata.obsm) + + mem_adata = run_from_json(self.json_file, save_to_disk=False) + self.assertIsInstance(mem_adata, ad.AnnData) + self.assertIn("X_tsne", mem_adata.obsm) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_umap_transformation_template.py b/tests/templates/test_umap_transformation_template.py index cf58784a..96cd2b8e 100644 --- a/tests/templates/test_umap_transformation_template.py +++ b/tests/templates/test_umap_transformation_template.py @@ -1,5 +1,10 @@ # tests/templates/test_umap_transformation_template.py -"""Unit tests for the UMAP transformation template.""" +""" +Real (non-mocked) unit test for the UMAP Transformation template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,13 +12,11 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -22,165 +25,77 @@ from spac.templates.umap_transformation_template import run_from_json -def mock_adata(n_cells: int = 10) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) - obs = pd.DataFrame({ - "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], - "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] - }) - x_mat = rng.normal(size=(n_cells, 5)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = [f"Gene{i}" for i in range(5)] - # Add a layer to test layer processing - adata.layers["arcsinh_z_scores"] = rng.normal(size=(n_cells, 5)) - return adata +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 20 cells, 5 genes for UMAP.""" + rng = np.random.default_rng(42) + X = rng.random((20, 5)) + obs = pd.DataFrame({"cell_type": ["A", "B"] * 10}) + var = pd.DataFrame(index=[f"Gene_{i}" for i in range(5)]) + return ad.AnnData(X=X, obs=obs, var=var) class TestUmapTransformationTemplate(unittest.TestCase): - """Unit tests for the UMAP transformation template.""" + """Real (non-mocked) tests for the UMAP transformation template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "output" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters from NIDAP template - self.params = { + params = { "Upstream_Analysis": self.in_file, - "Number_of_Neighbors": 5, # Small for fast test + "Table_to_Process": "Original", + "Number_of_Neighbors": 5, "Minimum_Distance_between_Points": 0.1, "Target_Dimension_Number": 2, "Computational_Metric": "euclidean", "Random_State": 0, "Transform_Seed": 42, - "Table_to_Process": "Original", - "Output_File": self.out_file, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters - with patch('spac.templates.umap_transformation_template.' - 'run_umap') as mock_umap: - # Mock run_umap to modify in-place and return the same object - def mock_run_umap_inplace(adata, **kwargs): - adata.obsm["X_umap"] = np.random.rand(adata.n_obs, 2) - return adata - - mock_umap.side_effect = mock_run_umap_inplace - - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - # Verify file was saved - self.assertTrue(len(result) > 0) - - # Verify run_umap was called with correct parameters - mock_umap.assert_called_once() - call_kwargs = mock_umap.call_args[1] - self.assertEqual(call_kwargs['n_neighbors'], 5) - self.assertEqual(call_kwargs['min_dist'], 0.1) - self.assertEqual(call_kwargs['n_components'], 2) - self.assertEqual(call_kwargs['metric'], 'euclidean') - self.assertEqual(call_kwargs['random_state'], 0) - self.assertEqual(call_kwargs['transform_seed'], 42) - self.assertIsNone(call_kwargs['layer']) # "Original" → None - self.assertTrue(call_kwargs['verbose']) - - # Test 2: Run without saving - with patch('spac.templates.umap_transformation_template.' - 'run_umap') as mock_umap: - # Mock run_umap to modify in-place and return the same object - def mock_run_umap_inplace(adata, **kwargs): - adata.obsm["X_umap"] = np.random.rand(adata.n_obs, 2) - return adata - - mock_umap.side_effect = mock_run_umap_inplace - - result_no_save = run_from_json( - self.params, save_results=False - ) - # Check appropriate return type based on template - self.assertIsInstance(result_no_save, ad.AnnData) - self.assertIn("X_umap", result_no_save.obsm) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - with patch('spac.templates.umap_transformation_template.' - 'run_umap') as mock_umap: - # Mock run_umap to modify in-place - def mock_run_umap_inplace(adata, **kwargs): - adata.obsm["X_umap"] = np.random.rand(adata.n_obs, 2) - return adata - - mock_umap.side_effect = mock_run_umap_inplace - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - def test_layer_parameter_handling(self) -> None: - """Test that layer parameter is handled correctly.""" - # Test with specific layer - params_with_layer = self.params.copy() - params_with_layer["Table_to_Process"] = "arcsinh_z_scores" - - with patch('spac.templates.umap_transformation_template.' - 'run_umap') as mock_umap: - # Mock run_umap to modify in-place - def mock_run_umap_inplace(adata, **kwargs): - adata.obsm["X_umap"] = np.random.rand(adata.n_obs, 2) - return adata - - mock_umap.side_effect = mock_run_umap_inplace - - run_from_json(params_with_layer, save_results=False) - - # Verify layer parameter was passed correctly - call_kwargs = mock_umap.call_args[1] - self.assertEqual(call_kwargs['layer'], "arcsinh_z_scores") - - def test_default_parameters(self) -> None: - """Test that default parameters from JSON template are used.""" - # Minimal params - only required fields - minimal_params = { - "Upstream_Analysis": self.in_file, - } - - with patch('spac.templates.umap_transformation_template.' - 'run_umap') as mock_umap: - # Mock run_umap to modify in-place - def mock_run_umap_inplace(adata, **kwargs): - adata.obsm["X_umap"] = np.random.rand(adata.n_obs, 2) - return adata - - mock_umap.side_effect = mock_run_umap_inplace - - run_from_json(minimal_params, save_results=False) - - # Verify defaults from JSON template were used - call_kwargs = mock_umap.call_args[1] - self.assertEqual(call_kwargs['n_neighbors'], 75) - self.assertEqual(call_kwargs['min_dist'], 0.1) - self.assertEqual(call_kwargs['n_components'], 2) - self.assertEqual(call_kwargs['metric'], 'euclidean') - self.assertEqual(call_kwargs['random_state'], 0) - self.assertEqual(call_kwargs['transform_seed'], 42) + def test_umap_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run UMAP and verify outputs. + + Validates: + 1. saved_files dict has 'analysis' key + 2. Pickle contains AnnData with 'X_umap' in .obsm + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, + ) + + self.assertIsInstance(saved_files, dict) + self.assertIn("analysis", saved_files) + + pkl_path = Path(saved_files["analysis"]) + self.assertTrue(pkl_path.exists()) + self.assertGreater(pkl_path.stat().st_size, 0) + + with open(pkl_path, "rb") as f: + result_adata = pickle.load(f) + self.assertIsInstance(result_adata, ad.AnnData) + self.assertIn("X_umap", result_adata.obsm) + + mem_adata = run_from_json(self.json_file, save_to_disk=False) + self.assertIsInstance(mem_adata, ad.AnnData) + self.assertIn("X_umap", mem_adata.obsm) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_umap_tsne_pca_template.py b/tests/templates/test_umap_tsne_pca_template.py index e1d9bd18..f04215f2 100644 --- a/tests/templates/test_umap_tsne_pca_template.py +++ b/tests/templates/test_umap_tsne_pca_template.py @@ -1,5 +1,10 @@ # tests/templates/test_umap_tsne_pca_template.py -"""Unit tests for the UMAP\\tSNE\\PCA Visualization template.""" +""" +Real (non-mocked) unit test for the UMAP/tSNE/PCA Visualization template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,175 +12,92 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import matplotlib -matplotlib.use("Agg") # Headless backend for CI +matplotlib.use("Agg") import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" ) -from spac.templates.umap_tsne_pca_template import run_from_json - - -def mock_adata(n_cells: int = 10) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) - obs = pd.DataFrame({ - "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], - "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] - }) - x_mat = rng.normal(size=(n_cells, 3)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = ["Gene1", "Gene2", "Gene3"] - # Add UMAP coordinates for visualization - adata.obsm["X_umap"] = rng.random((n_cells, 2)) * 10 - adata.obsm["X_tsne"] = rng.random((n_cells, 2)) * 50 - adata.obsm["X_pca"] = rng.random((n_cells, 2)) * 5 +from spac.templates.umap_tsne_pca_visualization_template import run_from_json + + +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData with pre-computed UMAP embedding for visualization.""" + rng = np.random.default_rng(42) + X = rng.random((8, 2)) + obs = pd.DataFrame({"cell_type": ["A", "B"] * 4}) + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + adata = ad.AnnData(X=X, obs=obs, var=var) + adata.obsm["X_umap"] = rng.random((8, 2)) * 10 return adata class TestUmapTsnePcaTemplate(unittest.TestCase): - """Unit tests for the UMAP\\tSNE\\PCA Visualization template.""" + """Real (non-mocked) tests for the UMAP/tSNE/PCA visualization.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "visualization" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters - adjust based on template - self.params = { + params = { "Upstream_Analysis": self.in_file, - "Annotation_to_Highlight": "cell_type", - "Feature_to_Highlight": "None", - "Table": "Original", - "Dimension_Reduction_Method": "umap", - "Figure_Width": 12, - "Figure_Height": 12, - "Font_Size": 12, - "Figure_DPI": 300, - "Legend_Location": "best", - "Legend_Font_Size": 16, - "Legend_Marker_Size": 5.0, + "Dimensionality_Reduction_Method": "UMAP", "Color_By": "Annotation", - "Dot_Size": 1, - "Value_Min": "None", - "Value_Max": "None", - "Output_File": self.out_file, + "Annotation": "cell_type", + "Feature": "None", + "Figure_Width": 6, + "Figure_Height": 4, + "Figure_DPI": 72, + "Font_Size": 10, + "Spot_Size": 50, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "figures": {"type": "directory", "name": "figures_dir"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Mock the dimensionality_reduction_plot function - with patch('spac.templates.umap_tsne_pca_template.' - 'dimensionality_reduction_plot') as mock_plot: - # Create mock figure and axis - mock_fig = MagicMock() - mock_ax = MagicMock() - mock_ax.get_figure.return_value = mock_fig - mock_ax.get_legend.return_value = MagicMock() - mock_fig.savefig = MagicMock() - - mock_plot.return_value = (mock_fig, mock_ax) - - # Test 1: Run with default parameters - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - # Check that output file has proper extension - output_files = list(result.keys()) - self.assertTrue( - any(f.endswith('.png') for f in output_files) - ) - - # Test 2: Run without saving - result_no_save = run_from_json( - self.params, save_results=False - ) - # Check appropriate return type based on template - self.assertIsInstance(result_no_save, tuple) - fig, df = result_no_save - self.assertEqual(fig, mock_fig) - # This template doesn't return a dataframe - self.assertIsNone(df) - - # Test 3: JSON file input - json_path = os.path.join( - self.tmp_dir.name, "params.json" - ) - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test invalid float conversion for vmin - params_bad = self.params.copy() - params_bad["Value_Min"] = "invalid_number" - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - # Check exact error message - expected_msg = ( - "Error: can't convert Value Min to float. " - "Received:\"invalid_number\"" + def test_umap_tsne_pca_visualization_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run dim reduction visualization and verify. + + Validates: + 1. saved_files dict has 'figures' key + 2. Figures directory contains non-empty PNG(s) + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + show_plot=False, + output_dir=self.tmp_dir.name, ) - self.assertEqual(str(context.exception), expected_msg) - - @patch('spac.templates.umap_tsne_pca_template.' - 'dimensionality_reduction_plot') - def test_function_calls(self, mock_plot) -> None: - """Test that main function is called with correct parameters.""" - # Mock the main function - mock_fig = MagicMock() - mock_ax = MagicMock() - mock_ax.get_figure.return_value = mock_fig - mock_ax.get_legend.return_value = None - mock_plot.return_value = (mock_fig, mock_ax) - - # Test with feature coloring instead of annotation - params_feature = self.params.copy() - params_feature["Color_By"] = "Feature" - params_feature["Feature_to_Highlight"] = "Gene1" - params_feature["Annotation_to_Highlight"] = "None" - params_feature["Value_Min"] = "0.5" - params_feature["Value_Max"] = "10.0" - - run_from_json(params_feature, save_results=False, show_plot=False) - - # Verify function was called correctly - mock_plot.assert_called_once() - call_args = mock_plot.call_args - - # Check that annotation is None and feature is set - self.assertIsNone(call_args[1]['annotation']) - self.assertEqual(call_args[1]['feature'], "Gene1") - self.assertEqual(call_args[1]['vmin'], 0.5) - self.assertEqual(call_args[1]['vmax'], 10.0) - self.assertEqual(call_args[1]['method'], "umap") - self.assertEqual(call_args[1]['point_size'], 1) + + self.assertIsInstance(saved_files, dict) + self.assertIn("figures", saved_files) + + figure_paths = saved_files["figures"] + self.assertGreaterEqual(len(figure_paths), 1) + for fig_path in figure_paths: + fig_file = Path(fig_path) + self.assertTrue(fig_file.exists()) + self.assertGreater(fig_file.stat().st_size, 0) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_utag_clustering_template.py b/tests/templates/test_utag_clustering_template.py index c734eafb..dc5f172d 100644 --- a/tests/templates/test_utag_clustering_template.py +++ b/tests/templates/test_utag_clustering_template.py @@ -1,5 +1,10 @@ # tests/templates/test_utag_clustering_template.py -"""Unit tests for the UTAG Clustering template.""" +""" +Real (non-mocked) unit test for the UTAG Clustering template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,13 +12,11 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" @@ -22,148 +25,85 @@ from spac.templates.utag_clustering_template import run_from_json -def mock_adata(n_cells: int = 50) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) - obs = pd.DataFrame({ - "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], - "slide_id": (["Slide1", "Slide2"] * ((n_cells + 1) // 2))[:n_cells] - }) - x_mat = rng.normal(size=(n_cells, 5)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = ["Marker1", "Marker2", "Marker3", "Marker4", "Marker5"] - # Add spatial coordinates required for UTAG - adata.obsm["spatial"] = rng.random((n_cells, 2)) * 100 +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 30 cells with spatial coords for UTAG clustering.""" + rng = np.random.default_rng(42) + X = rng.random((30, 3)) + obs = pd.DataFrame({"cell_type": ["A", "B", "C"] * 10}) + var = pd.DataFrame(index=["Gene_0", "Gene_1", "Gene_2"]) + spatial = rng.random((30, 2)) * 100 + adata = ad.AnnData(X=X, obs=obs, var=var) + adata.obsm["spatial"] = spatial return adata class TestUTAGClusteringTemplate(unittest.TestCase): - """Unit tests for the UTAG Clustering template.""" + """Real (non-mocked) tests for the UTAG clustering template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "output" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters - adjust based on template - self.params = { + params = { "Upstream_Analysis": self.in_file, "Table_to_Process": "Original", "Features": ["All"], "Slide_Annotation": "None", "Distance_Threshold": 20.0, - "K_Nearest_Neighbors": 15, + "K_Nearest_Neighbors": 5, "Resolution_Parameter": 1, "PCA_Components": "None", "Random_Seed": 42, "N_Jobs": 1, - "Leiden_Iterations": 5, - "Parallel_Processes": False, + "Leiden_Iterations": 3, + "Parellel_Processes": False, "Output_Annotation_Name": "UTAG", - "Output_File": self.out_file, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_complete_io_workflow(self) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Mock the run_utag_clustering function - with patch('spac.templates.utag_clustering_template.' - 'run_utag_clustering') as mock_utag: - # Mock function adds UTAG column to adata.obs - def add_utag_column(adata, **kwargs): - # Add mock UTAG clusters - n_cells = adata.n_obs - clusters = ["UTAG_" + str(i % 3) - for i in range(n_cells)] - adata.obs[kwargs['output_annotation']] = \ - pd.Categorical(clusters) - - mock_utag.side_effect = add_utag_column - - # Test 1: Run with default parameters - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - self.assertTrue(len(result) > 0) - - # Test 2: Run without saving - result_no_save = run_from_json( - self.params, save_results=False - ) - # Check appropriate return type based on template - self.assertIsInstance(result_no_save, ad.AnnData) - self.assertIn("UTAG", result_no_save.obs.columns) - - # Test 3: JSON file input - json_path = os.path.join( - self.tmp_dir.name, "params.json" - ) - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test invalid integer conversion for principal_components - params_bad = self.params.copy() - params_bad["PCA_Components"] = "invalid_number" - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - # Check exact error message - expected_msg = ( - "Error: can't convert principal_components to integer. " - "Received:\"invalid_number\"" + def test_utag_clustering_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run UTAG clustering and verify outputs. + + Validates: + 1. saved_files dict has 'analysis' key + 2. Pickle contains AnnData with UTAG obs column + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) - self.assertEqual(str(context.exception), expected_msg) - - @patch('spac.templates.utag_clustering_template.run_utag_clustering') - def test_function_calls(self, mock_utag) -> None: - """Test that main function is called with correct parameters.""" - # Mock the main function - def add_utag_column(adata, **kwargs): - adata.obs[kwargs['output_annotation']] = pd.Categorical( - ["UTAG_0"] * adata.n_obs - ) - - mock_utag.side_effect = add_utag_column - - # Test with specific features instead of "All" - params_features = self.params.copy() - params_features["Features"] = ["Marker1", "Marker3"] - params_features["Slide_Annotation"] = "slide_id" - params_features["PCA_Components"] = "10" - - run_from_json(params_features, save_results=False) - - # Verify function was called correctly - mock_utag.assert_called_once() - call_args = mock_utag.call_args - - # Check parameter conversions - self.assertEqual(call_args[1]['features'], ["Marker1", "Marker3"]) - self.assertEqual(call_args[1]['k'], 15) - self.assertEqual(call_args[1]['resolution'], 1) - self.assertEqual(call_args[1]['max_dist'], 20.0) - self.assertEqual(call_args[1]['n_pcs'], 10) - self.assertEqual(call_args[1]['slide_key'], "slide_id") - self.assertEqual(call_args[1]['layer'], None) # "Original" → None - self.assertEqual(call_args[1]['output_annotation'], "UTAG") - self.assertEqual(call_args[1]['parallel'], False) + + self.assertIsInstance(saved_files, dict) + self.assertIn("analysis", saved_files) + + pkl_path = Path(saved_files["analysis"]) + self.assertTrue(pkl_path.exists()) + self.assertGreater(pkl_path.stat().st_size, 0) + + with open(pkl_path, "rb") as f: + result_adata = pickle.load(f) + self.assertIsInstance(result_adata, ad.AnnData) + self.assertIn("UTAG", result_adata.obs.columns) + + mem_adata = run_from_json(self.json_file, save_to_disk=False) + self.assertIsInstance(mem_adata, ad.AnnData) + self.assertIn("UTAG", mem_adata.obs.columns) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_visualize_nearest_neighbor_template.py b/tests/templates/test_visualize_nearest_neighbor_template.py index 429dffd2..ba62948c 100644 --- a/tests/templates/test_visualize_nearest_neighbor_template.py +++ b/tests/templates/test_visualize_nearest_neighbor_template.py @@ -1,5 +1,10 @@ # tests/templates/test_visualize_nearest_neighbor_template.py -"""Unit tests for the Visualize Nearest Neighbor template.""" +""" +Real (non-mocked) unit test for the Visualize Nearest Neighbor template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,231 +12,119 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import matplotlib -matplotlib.use("Agg") # Headless backend for CI +matplotlib.use("Agg") import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" ) from spac.templates.visualize_nearest_neighbor_template import run_from_json +from spac.templates.nearest_neighbor_calculation_template import ( + run_from_json as run_nn, +) -def mock_adata(n_cells: int = 10) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) +def _make_adata_with_nn() -> ad.AnnData: + """Create AnnData with pre-computed nearest neighbor results.""" + rng = np.random.default_rng(42) + X = rng.random((12, 2)) obs = pd.DataFrame({ - "renamed_phenotypes": (["Tfh", "B_cells"] * ((n_cells + 1) // 2))[:n_cells], - "image_id": (["Image1", "Image2"] * ((n_cells + 1) // 2))[:n_cells] + "cell_type": ["A", "B", "C"] * 4, }) - x_mat = rng.normal(size=(n_cells, 3)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = ["Marker1", "Marker2", "Marker3"] - - # Add spatial coordinates - adata.obsm["spatial"] = rng.random((n_cells, 2)) * 100 - - # Add mock distance matrix with proper structure - # Create a mock distance matrix in obsm - # For nearest neighbor, we need distances from each phenotype - unique_phenotypes = obs["renamed_phenotypes"].unique() - n_phenotypes = len(unique_phenotypes) - - # Create random distance matrix - distance_matrix = rng.exponential(scale=10, size=(n_cells, n_phenotypes)) - distance_df = pd.DataFrame( - distance_matrix, - columns=[f"distance_to_{pheno}" for pheno in unique_phenotypes], - index=adata.obs.index # Use the same index as adata.obs - ) - adata.obsm["spatial_distance"] = distance_df - + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + spatial = rng.random((12, 2)) * 100 + adata = ad.AnnData(X=X, obs=obs, var=var) + adata.obsm["spatial"] = spatial + + # Run actual nearest neighbor to populate .obsm + import tempfile as tf + with tf.TemporaryDirectory() as td: + pkl_in = os.path.join(td, "in.pickle") + with open(pkl_in, "wb") as f: + pickle.dump(adata, f) + nn_params = { + "Upstream_Analysis": pkl_in, + "Annotation": "cell_type", + "ImageID": "None", + } + json_path = os.path.join(td, "p.json") + with open(json_path, "w") as f: + json.dump(nn_params, f) + adata = run_nn(json_path, save_to_disk=False) return adata class TestVisualizeNearestNeighborTemplate(unittest.TestCase): - """Unit tests for the Visualize Nearest Neighbor template.""" + """Real (non-mocked) tests for visualize nearest neighbor template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "nearest_neighbor_plots.csv" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_adata_with_nn(), f) - # Minimal parameters - adjust based on template - self.params = { + params = { "Upstream_Analysis": self.in_file, - "Annotation": "renamed_phenotypes", - "ImageID": "None", - "Plot_Method": "numeric", - "Plot_Type": "boxen", - "Source_Anchor_Cell_Label": "Tfh", - "Target_Cell_Label": "All", + "Annotation": "cell_type", + "Source_Anchor_Cell_Label": "A", "Nearest_Neighbor_Associated_Table": "spatial_distance", - "Log_Scale": False, - "Facet_Plot": False, - "X_Axis_Label_Rotation": 0, - "Shared_X_Axis_Title_": True, - "X_Axis_Title_Font_Size": "None", - "Defined_Color_Mapping": "None", - "Figure_Width": 12, - "Figure_Height": 6, - "Figure_DPI": 300, - "Font_Size": 12, - "Output_File": self.out_file, + "Figure_Width": 6, + "Figure_Height": 4, + "Figure_DPI": 72, + "Font_Size": 10, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "figures": {"type": "directory", "name": "figures"}, + "dataframe": {"type": "file", "name": "dataframe.csv"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - @patch('spac.templates.visualize_nearest_neighbor_template.' - 'visualize_nearest_neighbor') - def test_complete_io_workflow(self, mock_vis_nn) -> None: - """Single I/O test covering input/output scenarios.""" - # Mock the visualize_nearest_neighbor function - mock_fig = MagicMock() - mock_fig.number = 1 - mock_fig.set_size_inches = MagicMock() - mock_fig.set_dpi = MagicMock() - mock_fig.suptitle = MagicMock() - mock_fig.tight_layout = MagicMock() - mock_fig.supxlabel = MagicMock() - - mock_ax = MagicMock() - mock_ax.get_legend.return_value = None - mock_ax.get_xlabel.return_value = "distance" - mock_ax.set_xlabel = MagicMock() - mock_ax.xaxis.get_label.return_value = MagicMock() - - # Create mock dataframe with expected columns - mock_df = pd.DataFrame({ - 'group': ['B_cells', 'Tfh'] * 5, - 'distance': np.random.rand(10) * 10, - 'image_id': ['Image1'] * 5 + ['Image2'] * 5 - }) - - # Mock palette - mock_palette = { - 'B_cells': '#1f77b4', - 'Tfh': '#ff7f0e' - } - - mock_vis_nn.return_value = { - "data": mock_df, - "fig": mock_fig, - "palette": mock_palette, - "ax": mock_ax - } - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test 1: Run with default parameters - result = run_from_json(self.params, show_plot=False) - self.assertIsInstance(result, dict) - self.assertIn(self.out_file, result) - - # Test 2: Run without saving - result_no_save = run_from_json( - self.params, save_results=False, show_plot=False - ) - # Check appropriate return type - should be tuple - self.assertIsInstance(result_no_save, tuple) - self.assertEqual(len(result_no_save), 2) - # First element is figure, second is dataframe - self.assertIsInstance(result_no_save[1], pd.DataFrame) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result_json = run_from_json(json_path, show_plot=False) - self.assertIsInstance(result_json, dict) - - # Verify visualize_nearest_neighbor was called correctly - mock_vis_nn.assert_called() - call_args = mock_vis_nn.call_args[1] - self.assertEqual(call_args['annotation'], "renamed_phenotypes") - self.assertEqual(call_args['spatial_distance'], "spatial_distance") - self.assertEqual(call_args['distance_from'], "Tfh") - self.assertEqual(call_args['distance_to'], None) # "All" → None - self.assertEqual(call_args['method'], "numeric") - self.assertEqual(call_args['plot_type'], "boxen") - self.assertEqual(call_args['log'], False) - self.assertEqual(call_args['facet_plot'], False) - - def test_error_validation(self) -> None: - """Test exact error message for invalid parameters.""" - # Test invalid integer conversion for x_axis_title_fontsize - params_bad = self.params.copy() - params_bad["X_Axis_Title_Font_Size"] = "invalid_size" - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) - - # Check exact error message from text_to_value - expected_msg = ( - "Error: can't convert to integer. " - "Received:\"invalid_size\"" + def test_visualize_nn_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: visualize nearest neighbors and verify. + + Validates: + 1. saved_files dict has 'figures' and/or 'dataframe' keys + 2. Output files exist and are non-empty + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + show_plot=False, + output_dir=self.tmp_dir.name, ) - self.assertEqual(str(context.exception), expected_msg) - - @patch('spac.templates.visualize_nearest_neighbor_template.' - 'visualize_nearest_neighbor') - def test_function_calls(self, mock_vis_nn) -> None: - """Test that main function is called with correct parameters.""" - # Mock the main function - mock_fig = MagicMock() - mock_ax = MagicMock() - mock_ax.get_legend.return_value = None - mock_ax.get_xlabel.return_value = "" - - mock_df = pd.DataFrame({ - 'group': ['B_cells', 'Tfh', 'Stroma'] * 2, - 'distance': [5.0, 7.0, 3.0, 6.0, 8.0, 4.0] - }) - - mock_vis_nn.return_value = { - "data": mock_df, - "fig": mock_fig, - "palette": {'B_cells': '#000', 'Tfh': '#fff', 'Stroma': '#ccc'}, - "ax": mock_ax - } - - # Test with different parameters - params_alt = self.params.copy() - params_alt["Target_Cell_Label"] = "B_cells,Stroma" - params_alt["Log_Scale"] = True - params_alt["Facet_Plot"] = True - params_alt["ImageID"] = "image_id" - params_alt["X_Axis_Label_Rotation"] = 45 - - run_from_json(params_alt, save_results=False, show_plot=False) - - # Verify function was called correctly - mock_vis_nn.assert_called_once() - call_args = mock_vis_nn.call_args[1] - - # Check specific parameter conversions - self.assertEqual(call_args['distance_to'], ["B_cells", "Stroma"]) - self.assertEqual(call_args['log'], True) - self.assertEqual(call_args['facet_plot'], True) - self.assertEqual(call_args['stratify_by'], "image_id") + + self.assertIsInstance(saved_files, dict) + self.assertGreater(len(saved_files), 0) + + if "figures" in saved_files: + figure_paths = saved_files["figures"] + self.assertGreaterEqual(len(figure_paths), 1) + for fig_path in figure_paths: + fig_file = Path(fig_path) + self.assertTrue(fig_file.exists()) + self.assertGreater(fig_file.stat().st_size, 0) + + if "dataframe" in saved_files: + csv_path = Path(saved_files["dataframe"]) + self.assertTrue(csv_path.exists()) + self.assertGreater(csv_path.stat().st_size, 0) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_visualize_ripley_template.py b/tests/templates/test_visualize_ripley_template.py index 6129cfc4..c7ada182 100644 --- a/tests/templates/test_visualize_ripley_template.py +++ b/tests/templates/test_visualize_ripley_template.py @@ -1,5 +1,10 @@ # tests/templates/test_visualize_ripley_template.py -"""Unit tests for the Visualize Ripley L template.""" +""" +Real (non-mocked) unit test for the Visualize Ripley L template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,211 +12,123 @@ import sys import tempfile import unittest -import warnings +from pathlib import Path import matplotlib -matplotlib.use("Agg") # Headless backend for CI +matplotlib.use("Agg") import anndata as ad import numpy as np import pandas as pd -from pathlib import Path -from unittest.mock import patch, MagicMock sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" ) -from spac.templates.visualize_ripley_template import run_from_json - - -def mock_adata_with_ripley(n_cells: int = 10) -> ad.AnnData: - """Return a minimal synthetic AnnData with Ripley L results for tests.""" - rng = np.random.default_rng(0) - obs = pd.DataFrame({ - "phenotype": ["B cells", "CD8 T cells"] * (n_cells // 2) - }) - x_mat = rng.normal(size=(n_cells, 2)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.obsm["spatial"] = rng.random((n_cells, 2)) * 50.0 - - # Add mock Ripley L results in the expected format - # When using pickle, the structure is preserved as-is - adata.uns["ripley_l_B cells_CD8 T cells"] = { - "radius": [0, 50, 100], - "ripley_l": [0, 1.2, 2.5], - "simulations": np.array([ - [0, 0.8, 1.9], [0, 1.1, 2.3], [0, 1.3, 2.7] - ]) - } +from spac.templates.visualize_ripley_l_template import run_from_json +from spac.templates.ripley_l_calculation_template import ( + run_from_json as run_ripley, +) + + +def _make_adata_with_ripley() -> ad.AnnData: + """Create AnnData with pre-computed Ripley L results in .uns.""" + rng = np.random.default_rng(42) + X = rng.random((20, 2)) + obs = pd.DataFrame({"cell_type": (["A"] * 10) + (["B"] * 10)}) + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + spatial = rng.random((20, 2)) * 100 + adata = ad.AnnData(X=X, obs=obs, var=var) + adata.obsm["spatial"] = spatial + + # Run actual Ripley L to populate .uns + import tempfile as tf + with tf.TemporaryDirectory() as td: + pkl_in = os.path.join(td, "in.pickle") + with open(pkl_in, "wb") as f: + pickle.dump(adata, f) + ripley_params = { + "Upstream_Analysis": pkl_in, + "Radii": [5, 10, 20], + "Annotation": "cell_type", + "Center_Phenotype": "A", + "Neighbor_Phenotype": "B", + "Number_of_Simulations": 5, + "Seed": 42, + "Spatial_Key": "spatial", + "Edge_Correction": True, + } + json_path = os.path.join(td, "p.json") + with open(json_path, "w") as f: + json.dump(ripley_params, f) + adata = run_ripley(json_path, save_to_disk=False) return adata class TestVisualizeRipleyTemplate(unittest.TestCase): - """Unit tests for the Visualize Ripley L template.""" + """Real (non-mocked) tests for the visualize Ripley L template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "ripley_output.pickle" - ) - self.out_file = "plots.csv" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data with Ripley results as pickle - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata_with_ripley(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_adata_with_ripley(), f) - # Minimal parameters - self.params = { + params = { "Upstream_Analysis": self.in_file, - "Center_Phenotype": "B cells", - "Neighbor_Phenotype": "CD8 T cells", - "Plot_Specific_Regions": False, - "Regions_Labels": [], - "Plot_Simulations": True, - "Output_File": self.out_file, + "Radii": [5, 10, 20], + "Annotation": "cell_type", + "Center_Phenotype": "A", + "Neighbor_Phenotype": "B", + "Figure_Width": 6, + "Figure_Height": 4, + "Figure_DPI": 72, + "Font_Size": 10, + "Output_Directory": self.tmp_dir.name, + "outputs": { + "figures": {"type": "directory", "name": "figures_dir"}, + "dataframe": {"type": "file", "name": "dataframe.csv"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - @patch('spac.templates.visualize_ripley_template.plot_ripley_l') - def test_run_with_save(self, mock_plot_ripley) -> None: - """Test visualization with file saving.""" - # Mock the plot_ripley_l function to return a figure and dataframe - mock_fig = MagicMock() - mock_df = pd.DataFrame({ - 'radius': [0, 50, 100], - 'ripley_l': [0, 1.2, 2.5], - 'lower_ci': [0, 0.8, 1.9], - 'upper_ci': [0, 1.6, 3.1] - }) - mock_plot_ripley.return_value = (mock_fig, mock_df) - - # Suppress warnings for cleaner test output - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Test with save_results=True (default) - saved_files = run_from_json(self.params) - self.assertIn(self.out_file, saved_files) - - # Verify plot_ripley_l was called with correct parameters - mock_plot_ripley.assert_called_once() - call_args = mock_plot_ripley.call_args - # Check that adata was passed as first argument - self.assertEqual(call_args[0][0].n_obs, 10) - # Check keyword arguments - self.assertEqual( - call_args[1]['phenotypes'], ("B cells", "CD8 T cells") - ) - self.assertEqual(call_args[1]['regions'], None) - self.assertEqual(call_args[1]['sims'], True) - self.assertEqual(call_args[1]['return_df'], True) - - @patch('spac.templates.visualize_ripley_template.plot_ripley_l') - def test_run_without_save(self, mock_plot_ripley) -> None: - """Test visualization without file saving.""" - # Mock the plot_ripley_l function - mock_fig = MagicMock() - mock_df = pd.DataFrame({ - 'radius': [0, 50, 100], - 'ripley_l': [0, 1.2, 2.5] - }) - mock_plot_ripley.return_value = (mock_fig, mock_df) - - # Test with save_results=False - fig, df = run_from_json(self.params, save_results=False) - - # Verify we got the figure and dataframe back - self.assertEqual(fig, mock_fig) - self.assertIsInstance(df, pd.DataFrame) - self.assertEqual(len(df), 3) - self.assertIn('radius', df.columns) - self.assertIn('ripley_l', df.columns) - - @patch('spac.templates.visualize_ripley_template.plot_ripley_l') - def test_with_specific_regions(self, mock_plot_ripley) -> None: - """Test with specific regions enabled.""" - params_regions = self.params.copy() - params_regions["Plot_Specific_Regions"] = True - params_regions["Regions_Labels"] = ["Region1", "Region2"] - - mock_fig = MagicMock() - mock_df = pd.DataFrame({'radius': [0], 'ripley_l': [0]}) - mock_plot_ripley.return_value = (mock_fig, mock_df) - - run_from_json(params_regions, save_results=False) - - # Verify regions parameter was passed correctly - call_args = mock_plot_ripley.call_args - self.assertEqual( - call_args[1]['regions'], ["Region1", "Region2"] - ) - - def test_regions_validation_error_message(self) -> None: + def test_visualize_ripley_produces_expected_outputs(self) -> None: """ - Test exact error message for empty regions - when Plot_Specific_Regions is True. - """ - params_bad = self.params.copy() - params_bad["Plot_Specific_Regions"] = True - params_bad["Regions_Labels"] = [] - - with self.assertRaises(ValueError) as context: - run_from_json(params_bad) + End-to-end I/O test: visualize Ripley L and verify outputs. - expected_msg = ( - 'Please identify at least one region in the ' - '"Regions Label(s) parameter' - ) - actual_msg = str(context.exception) - self.assertEqual(expected_msg, actual_msg) - - @patch('spac.templates.visualize_ripley_template.plot_ripley_l') - @patch('builtins.print') - def test_console_output(self, mock_print, mock_plot_ripley) -> None: - """Test that dataframe is printed to console.""" - # Mock the plot_ripley_l function - mock_fig = MagicMock() - mock_df = pd.DataFrame({ - 'radius': [0, 50, 100], - 'ripley_l': [0, 1.2, 2.5] - }) - mock_plot_ripley.return_value = (mock_fig, mock_df) - - run_from_json(self.params, save_results=False) - - # Verify dataframe was printed - print_calls = mock_print.call_args_list - # Check that to_string() output was printed - df_printed = False - for call in print_calls: - if (len(call[0]) > 0 and - str(call[0][0]) == mock_df.to_string()): - df_printed = True - break - self.assertTrue( - df_printed, "DataFrame was not printed to console" + Validates: + 1. saved_files dict has output keys + 2. Output files exist and are non-empty + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + show_plot=False, + output_dir=self.tmp_dir.name, ) - @patch('spac.templates.visualize_ripley_template.plot_ripley_l') - def test_json_file_input(self, mock_plot_ripley) -> None: - """Test with JSON file input.""" - mock_fig = MagicMock() - mock_df = pd.DataFrame({'radius': [0], 'ripley_l': [0]}) - mock_plot_ripley.return_value = (mock_fig, mock_df) - - json_path = os.path.join(self.tmp_dir.name, "viz_params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - result = run_from_json(json_path, save_results=False) - - # Should return tuple when save_results=False - self.assertIsInstance(result, tuple) - self.assertEqual(len(result), 2) + self.assertIsInstance(saved_files, dict) + # Check that at least some output was produced + self.assertGreater(len(saved_files), 0) + + for key, value in saved_files.items(): + if isinstance(value, list): + for p in value: + pf = Path(p) + self.assertTrue(pf.exists()) + self.assertGreater(pf.stat().st_size, 0) + elif isinstance(value, str): + pf = Path(value) + self.assertTrue(pf.exists()) + self.assertGreater(pf.stat().st_size, 0) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/templates/test_zscore_normalization_template.py b/tests/templates/test_zscore_normalization_template.py index 27099de4..6c0049e7 100644 --- a/tests/templates/test_zscore_normalization_template.py +++ b/tests/templates/test_zscore_normalization_template.py @@ -1,5 +1,10 @@ # tests/templates/test_zscore_normalization_template.py -"""Unit tests for the Z-Score Normalization template.""" +""" +Real (non-mocked) unit test for the Z-Score Normalization template. + +Validates template I/O behaviour only. +No mocking. Uses real data, real filesystem, and tempfile. +""" import json import os @@ -7,205 +12,87 @@ import sys import tempfile import unittest -import warnings -from unittest.mock import patch, MagicMock +from pathlib import Path import anndata as ad import numpy as np import pandas as pd -from pathlib import Path sys.path.append( os.path.dirname(os.path.realpath(__file__)) + "/../../src" ) -from spac.templates.zscore_normalization_template import run_from_json +from spac.templates.z_score_normalization_template import run_from_json -def mock_adata(n_cells: int = 10) -> ad.AnnData: - """Return a minimal synthetic AnnData for fast tests.""" - rng = np.random.default_rng(0) - obs = pd.DataFrame({ - "cell_type": (["TypeA", "TypeB"] * ((n_cells + 1) // 2))[:n_cells], - "sample": (["S1", "S2"] * ((n_cells + 1) // 2))[:n_cells] - }) - x_mat = rng.normal(size=(n_cells, 3)) - adata = ad.AnnData(X=x_mat, obs=obs) - adata.var_names = ["Marker1", "Marker2", "Marker3"] - - # Add a test layer with different values - adata.layers["test_layer"] = rng.normal(loc=5, scale=2, size=(n_cells, 3)) - - return adata +def _make_tiny_adata() -> ad.AnnData: + """Minimal AnnData: 4 cells, 2 genes for z-score normalization.""" + rng = np.random.default_rng(42) + X = rng.integers(1, 100, size=(4, 2)).astype(float) + obs = pd.DataFrame({"cell_type": ["A", "B", "A", "B"]}) + var = pd.DataFrame(index=["Gene_0", "Gene_1"]) + return ad.AnnData(X=X, obs=obs, var=var) class TestZScoreNormalizationTemplate(unittest.TestCase): - """Unit tests for the Z-Score Normalization template.""" + """Real (non-mocked) tests for the z-score normalization template.""" def setUp(self) -> None: self.tmp_dir = tempfile.TemporaryDirectory() - self.in_file = os.path.join( - self.tmp_dir.name, "input.pickle" - ) - self.out_file = "normalized_output" + self.in_file = os.path.join(self.tmp_dir.name, "input.pickle") - # Save minimal mock data - with open(self.in_file, 'wb') as f: - pickle.dump(mock_adata(), f) + with open(self.in_file, "wb") as f: + pickle.dump(_make_tiny_adata(), f) - # Minimal parameters from JSON template - self.params = { + params = { "Upstream_Analysis": self.in_file, "Table_to_Process": "Original", - "Output_Table_Name": "z_scores", - "Output_File": self.out_file, + "Output_Table_Name": "zscore", + "Output_Directory": self.tmp_dir.name, + "outputs": { + "analysis": {"type": "file", "name": "output.pickle"}, + }, } + self.json_file = os.path.join(self.tmp_dir.name, "params.json") + with open(self.json_file, "w") as f: + json.dump(params, f) + def tearDown(self) -> None: self.tmp_dir.cleanup() - @patch('spac.templates.zscore_normalization_template.' - 'z_score_normalization') - def test_complete_io_workflow(self, mock_z_score_norm) -> None: - """Single I/O test covering input/output scenarios.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Mock the z_score_normalization function - def mock_z_score_side_effect(adata, **kwargs): - # Simulate what z_score_normalization does - output_layer = kwargs.get('output_layer', 'z_scores') - input_layer = kwargs.get('input_layer', None) - - # Create normalized data - if input_layer is None: - data = adata.X - else: - data = adata.layers[input_layer] - - # Simple z-score normalization simulation - normalized = (data - np.mean(data, axis=0)) / np.std(data, axis=0) - adata.layers[output_layer] = normalized - return None - - mock_z_score_norm.side_effect = mock_z_score_side_effect - - # Test 1: Run with default parameters (Original layer) - result = run_from_json(self.params) - self.assertIsInstance(result, dict) - - # Verify z_score_normalization was called correctly - mock_z_score_norm.assert_called_once() - call_args = mock_z_score_norm.call_args - self.assertEqual(call_args[1]['output_layer'], "z_scores") - # Original -> None - self.assertEqual(call_args[1]['input_layer'], None) - - # Test 2: Run without saving - mock_z_score_norm.reset_mock() - result_no_save = run_from_json(self.params, save_results=False) - self.assertIsInstance(result_no_save, ad.AnnData) - self.assertIn("z_scores", result_no_save.layers) - - # Test 3: JSON file input - json_path = os.path.join(self.tmp_dir.name, "params.json") - with open(json_path, "w") as f: - json.dump(self.params, f) - - mock_z_score_norm.reset_mock() - result_json = run_from_json(json_path) - self.assertIsInstance(result_json, dict) - - # Test 4: Using a specific layer - params_with_layer = self.params.copy() - params_with_layer["Table_to_Process"] = "test_layer" - params_with_layer["Output_Table_Name"] = "test_z_scores" - - mock_z_score_norm.reset_mock() - result_layer = run_from_json(params_with_layer, save_results=False) - - # Verify correct layer was used - call_args = mock_z_score_norm.call_args - self.assertEqual(call_args[1]['input_layer'], "test_layer") - self.assertEqual(call_args[1]['output_layer'], "test_z_scores") - - @patch('builtins.print') - @patch('spac.templates.zscore_normalization_template.' - 'z_score_normalization') - def test_console_output( - self, mock_z_score_norm, mock_print - ) -> None: - """Test that summary statistics are printed to console.""" - # Mock z_score_normalization to create the expected layer - def mock_z_score_side_effect(adata, **kwargs): - output_layer = kwargs.get('output_layer', 'z_scores') - # Create dummy normalized data - adata.layers[output_layer] = np.zeros_like(adata.X) - return None - - mock_z_score_norm.side_effect = mock_z_score_side_effect - - run_from_json(self.params, save_results=False) - - # Check that describe() output was printed - print_calls = [ - str(call[0][0]) for call in mock_print.call_args_list - if call[0] - ] - - # Should contain statistical summary - summary_printed = any( - 'mean' in str(call).lower() or - 'std' in str(call).lower() or - 'count' in str(call).lower() - for call in print_calls - ) - self.assertTrue( - summary_printed, - "DataFrame summary statistics were not printed" + def test_zscore_normalization_produces_expected_outputs(self) -> None: + """ + End-to-end I/O test: run z-score normalization and verify outputs. + + Validates: + 1. saved_files dict has 'analysis' key + 2. Pickle exists, is non-empty, contains AnnData + 3. Z-score layer is present in the AnnData + """ + saved_files = run_from_json( + self.json_file, + save_to_disk=True, + output_dir=self.tmp_dir.name, ) - - # Should print adata info - adata_printed = any( - 'AnnData' in str(call) for call in print_calls - ) - self.assertTrue(adata_printed, "AnnData info was not printed") - - def test_missing_upstream_analysis_error(self) -> None: - """Test exact error for missing Upstream_Analysis parameter.""" - params_bad = self.params.copy() - del params_bad["Upstream_Analysis"] - - with self.assertRaises(KeyError) as context: - run_from_json(params_bad) - - self.assertIn("Upstream_Analysis", str(context.exception)) - - @patch('spac.templates.zscore_normalization_template.' - 'z_score_normalization') - def test_output_file_extension_handling( - self, mock_z_score_norm - ) -> None: - """Test that output defaults to pickle format.""" - # Mock z_score_normalization to create the expected layer - def mock_z_score_side_effect(adata, **kwargs): - output_layer = kwargs.get('output_layer', 'z_scores') - # Create dummy normalized data - adata.layers[output_layer] = np.zeros_like(adata.X) - return None - - mock_z_score_norm.side_effect = mock_z_score_side_effect - - params = self.params.copy() - params["Output_File"] = "results.dat" # No standard extension - - saved_files = run_from_json(params) - - # Should save as pickle by default - pickle_files = [f for f in saved_files.values() - if '.pickle' in str(f)] - self.assertTrue(len(pickle_files) > 0) + + self.assertIsInstance(saved_files, dict) + self.assertIn("analysis", saved_files) + + pkl_path = Path(saved_files["analysis"]) + self.assertTrue(pkl_path.exists()) + self.assertGreater(pkl_path.stat().st_size, 0) + + with open(pkl_path, "rb") as f: + result_adata = pickle.load(f) + self.assertIsInstance(result_adata, ad.AnnData) + # z-score normalization creates a 'zscore' layer + self.assertIn("zscore", result_adata.layers) + + mem_adata = run_from_json(self.json_file, save_to_disk=False) + self.assertIsInstance(mem_adata, ad.AnnData) + self.assertIn("zscore", mem_adata.layers) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From 7a5ec6d57ca7934d7d1003d417b3907f6d692308 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:57:23 -0500 Subject: [PATCH 100/102] fix: add missing 'import os' in performance tests - test_boxplot_performance.py - test_histogram_performance.py --- tests/test_performance/test_boxplot_performance.py | 1 + tests/test_performance/test_histogram_performance.py | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/test_performance/test_boxplot_performance.py b/tests/test_performance/test_boxplot_performance.py index 0cb7b54a..1e3ea434 100644 --- a/tests/test_performance/test_boxplot_performance.py +++ b/tests/test_performance/test_boxplot_performance.py @@ -1,3 +1,4 @@ +import os import unittest import time import numpy as np diff --git a/tests/test_performance/test_histogram_performance.py b/tests/test_performance/test_histogram_performance.py index 458fbbe5..308e80b3 100644 --- a/tests/test_performance/test_histogram_performance.py +++ b/tests/test_performance/test_histogram_performance.py @@ -1,3 +1,4 @@ +import os import unittest import time import warnings From d0bbc5ea8a7151bbf5f389549612b01862d6f382 Mon Sep 17 00:00:00 2001 From: fangliu117 <107778735+fangliu117@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:55:07 -0500 Subject: [PATCH 101/102] fix: Remove 6 deprecated templates (sync with tools_refactor) --- .../templates/add_pin_color_rule_template.py | 108 ---------- .../templates/load_csv_files_with_config.py | 98 --------- src/spac/templates/ripley_l_template.py | 135 ------------- src/spac/templates/umap_tsne_pca_template.py | 188 ------------------ .../templates/visualize_ripley_template.py | 121 ----------- .../zscore_normalization_template.py | 109 ---------- 6 files changed, 759 deletions(-) delete mode 100644 src/spac/templates/add_pin_color_rule_template.py delete mode 100644 src/spac/templates/load_csv_files_with_config.py delete mode 100644 src/spac/templates/ripley_l_template.py delete mode 100644 src/spac/templates/umap_tsne_pca_template.py delete mode 100644 src/spac/templates/visualize_ripley_template.py delete mode 100644 src/spac/templates/zscore_normalization_template.py diff --git a/src/spac/templates/add_pin_color_rule_template.py b/src/spac/templates/add_pin_color_rule_template.py deleted file mode 100644 index b6899102..00000000 --- a/src/spac/templates/add_pin_color_rule_template.py +++ /dev/null @@ -1,108 +0,0 @@ -""" -Platform-agnostic Append Pin Color Rule template converted from NIDAP. -Maintains the exact logic from the NIDAP template. - -Usage ------ ->>> from spac.templates.add_pin_color_rule_template import run_from_json ->>> run_from_json("examples/add_pin_color_rule_params.json") -""" -import json -import sys -from pathlib import Path -from typing import Any, Dict, Union - -# Add parent directory to path for imports -sys.path.append(str(Path(__file__).parent.parent.parent)) - -from spac.data_utils import add_pin_color_rules -from spac.templates.template_utils import ( - load_input, - save_outputs, - parse_params, - string_list_to_dictionary, -) - - -def run_from_json( - json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True -) -> Union[Dict[str, str], Any]: - """ - Execute Append Pin Color Rule analysis with parameters from JSON. - Replicates the NIDAP template functionality exactly. - - Parameters - ---------- - json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the adata object - directly for in-memory workflows. Default is True. - - Returns - ------- - dict or AnnData - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed AnnData object - """ - # Parse parameters from JSON - params = parse_params(json_path) - - # Load the upstream analysis data - adata = load_input(params["Upstream_Analysis"]) - - # Extract parameters - color_dict_string_list = params.get("Label_Color_Map", []) - color_map_name = params.get("Color_Map_Name", "_spac_colors") - overwrite = params.get("Overwrite_Previous_Color_Map", True) - - color_dict = string_list_to_dictionary( - color_dict_string_list, - key_name="label", - value_name="color" - ) - - add_pin_color_rules( - adata, - label_color_dict=color_dict, - color_map_name=color_map_name, - overwrite=overwrite - ) - print(adata.uns[f'{color_map_name}_summary']) - - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "color_mapped_analysis.pickle") - # Default to pickle format if no recognized extension - if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): - output_file = output_file + '.pickle' - - saved_files = save_outputs({output_file: adata}) - - print(f"Append Pin Color Rule completed → {saved_files[output_file]}") - return saved_files - else: - # Return the adata object directly for in-memory workflows - print("Returning AnnData object (not saving to file)") - return adata - - -# CLI interface -if __name__ == "__main__": - if len(sys.argv) != 2: - print( - "Usage: python add_pin_color_rule_template.py ", - file=sys.stderr - ) - sys.exit(1) - - result = run_from_json(sys.argv[1]) - - if isinstance(result, dict): - print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") - else: - print("\nReturned AnnData object") \ No newline at end of file diff --git a/src/spac/templates/load_csv_files_with_config.py b/src/spac/templates/load_csv_files_with_config.py deleted file mode 100644 index fc0d192f..00000000 --- a/src/spac/templates/load_csv_files_with_config.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -Platform-agnostic Load CSV Files template converted from NIDAP. -Maintains the exact logic from the NIDAP template. - -Usage ------ ->>> from spac.templates.load_csv_files_with_config import run_from_json ->>> run_from_json("examples/load_csv_params.json") -""" -import json -import sys -from pathlib import Path -from typing import Any, Dict, Union, List, Optional -import pandas as pd -import logging - -# Add parent directory to path for imports -sys.path.append(str(Path(__file__).parent.parent.parent)) - -from spac.templates.template_utils import ( - save_outputs, - parse_params, - text_to_value, - load_csv_files -) - - -def run_from_json( - json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True -) -> Union[Dict[str, str], pd.DataFrame]: - """ - Execute Load CSV Files analysis with parameters from JSON. - Replicates the NIDAP template functionality exactly. - - Parameters - ---------- - json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the DataFrame - directly for in-memory workflows. Default is True. - - Returns - ------- - dict or DataFrame - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed DataFrame - """ - # Parse parameters from JSON - params = parse_params(json_path) - - # Extract parameters - csv_dir = Path(params["CSV_Files"]) - files_config = pd.read_csv(params["CSV_Files_Configuration"]) - string_columns = params.get("String_Columns", [""]) - - # Load and combine CSV files - final_df = load_csv_files( - csv_dir=csv_dir, - files_config=files_config, - string_columns=string_columns - ) - - if final_df is None: - raise RuntimeError("Failed to process CSV files") - - print("Load CSV Files completed successfully.") - - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "combined_data.csv") - saved_files = save_outputs({output_file: final_df}) - - logging.info(f"Load CSV completed → {saved_files[output_file]}") - return saved_files - else: - # Return the DataFrame directly for in-memory workflows - logging.info("Returning DataFrame (not saving to file)") - return final_df - - -# CLI interface -if __name__ == "__main__": - if len(sys.argv) != 2: - print( - "Usage: python load_csv_template.py ", - file=sys.stderr - ) - sys.exit(1) - - saved_files = run_from_json(sys.argv[1]) - - if isinstance(saved_files, dict): - print("\nOutput files:") - for filename, filepath in saved_files.items(): - print(f" {filename}: {filepath}") \ No newline at end of file diff --git a/src/spac/templates/ripley_l_template.py b/src/spac/templates/ripley_l_template.py deleted file mode 100644 index a787b3ac..00000000 --- a/src/spac/templates/ripley_l_template.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -Platform-agnostic Ripley-L template converted from NIDAP. -Maintains the exact logic from the NIDAP template. - -Usage ------ ->>> from spac.templates.ripley_l_template import run_from_json ->>> run_from_json("examples/ripley_l_params.json") -""" -import json -import sys -from pathlib import Path -from typing import Any, Dict, Union, List -import logging - -# Add parent directory to path for imports -sys.path.append(str(Path(__file__).parent.parent.parent)) - -from spac.spatial_analysis import ripley_l -from spac.templates.template_utils import ( - load_input, - save_outputs, - parse_params, - text_to_value, - convert_to_floats -) - - -def run_from_json( - json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True -) -> Union[Dict[str, str], Any]: - """ - Execute Ripley-L analysis with parameters from JSON. - Replicates the NIDAP template functionality exactly. - - Parameters - ---------- - json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the adata object - directly for in-memory workflows. Default is True. - - Returns - ------- - dict or AnnData - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed AnnData object - """ - # Parse parameters from JSON - params = parse_params(json_path) - - # Load the upstream analysis data - adata = load_input(params["Upstream_Analysis"]) - - # Extract parameters - radii = params["Radii"] - annotation = params["Annotation"] - phenotypes = [params["Center_Phenotype"], params["Neighbor_Phenotype"]] - regions = params.get("Stratify_By", "None") - n_simulations = params.get("Number_of_Simulations", 100) - area = params.get("Area", "None") - seed = params.get("Seed", 42) - spatial_key = params.get("Spatial_Key", "spatial") - edge_correction = params.get("Edge_Correction", True) - - # Process parameters - regions = text_to_value( - regions, - default_none_text="None" - ) - - area = text_to_value( - area, - default_none_text="None", - value_to_convert_to=None, - to_float=True, - param_name='Area' - ) - - # Convert radii to floats - radii = convert_to_floats(radii) - - # Run the analysis - ripley_l( - adata, - annotation=annotation, - phenotypes=phenotypes, - distances=radii, - regions=regions, - n_simulations=n_simulations, - area=area, - seed=seed, - spatial_key=spatial_key, - edge_correction=edge_correction - ) - - print("Ripley-L analysis completed successfully.") - - # Handle results based on save_results flag - if save_results: - # Save outputs - outfile = params.get("Output_File", "transform_output.pickle") - # Default to pickle format if not specified - if not outfile.endswith(('.pickle', '.pkl', '.h5ad')): - outfile = outfile.replace('.h5ad', '.pickle') - - logging.debug(f"Output file type: {type(outfile)}") - saved_files = save_outputs({outfile: adata}) - logging.debug(f"Saved files: {saved_files}") - - logging.info(f"Ripley-L completed → {str(saved_files[outfile])}") - logging.debug(f"AnnData object: {adata}") - return saved_files - else: - # Return the adata object directly for in-memory workflows - logging.info("Returning AnnData object (not saving to file)") - return adata - - -# CLI interface -if __name__ == "__main__": - if len(sys.argv) != 2: - print("Usage: python ripley_l_template.py ", file=sys.stderr) - sys.exit(1) - - saved_files = run_from_json(sys.argv[1]) - - if isinstance(saved_files, dict): - print("\nOutput files:") - for filename, filepath in saved_files.items(): - print(f" {filename}: {filepath}") - else: - print("\nReturned AnnData object") diff --git a/src/spac/templates/umap_tsne_pca_template.py b/src/spac/templates/umap_tsne_pca_template.py deleted file mode 100644 index 49fac9f7..00000000 --- a/src/spac/templates/umap_tsne_pca_template.py +++ /dev/null @@ -1,188 +0,0 @@ -""" -Platform-agnostic UMAP\\tSNE\\PCA Visualization template converted from NIDAP. -Maintains the exact logic from the NIDAP template. - -Usage ------ ->>> from spac.templates.umap_tsne_pca_template import run_from_json ->>> run_from_json("examples/umap_tsne_pca_params.json") -""" -import json -import sys -from pathlib import Path -from typing import Any, Dict, Union, Optional, Tuple -import pandas as pd -import matplotlib.pyplot as plt - -# Add parent directory to path for imports -sys.path.append(str(Path(__file__).parent.parent.parent)) - -from spac.visualization import dimensionality_reduction_plot -from spac.templates.template_utils import ( - load_input, - save_outputs, - parse_params, - text_to_value, -) - - -def run_from_json( - json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True, - show_plot: bool = True -) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: - """ - Execute UMAP\\tSNE\\PCA Visualization analysis with parameters from JSON. - Replicates the NIDAP template functionality exactly. - - Parameters - ---------- - json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the figure and - dataframe directly for in-memory workflows. Default is True. - show_plot : bool, optional - Whether to display the plot. Default is True. - - Returns - ------- - dict or tuple - If save_results=True: Dictionary of saved file paths - If save_results=False: Tuple of (figure, dataframe) - """ - # Parse parameters from JSON - params = parse_params(json_path) - - # Load the upstream analysis data - adata = load_input(params["Upstream_Analysis"]) - - # Extract parameters - annotation = params.get("Annotation_to_Highlight", "None") - feature = params.get("Feature_to_Highlight", "None") - layer = params.get("Table", "Original") - method = params.get("Dimension_Reduction_Method", "umap") - fig_width = params.get("Figure_Width", 12) - fig_height = params.get("Figure_Height", 12) - font_size = params.get("Font_Size", 12) - fig_dpi = params.get("Figure_DPI", 300) - legend_location = params.get("Legend_Location", "best") - legend_label_size = params.get("Legend_Font_Size", 16) - legend_marker_scale = params.get("Legend_Marker_Size", 5.0) - color_by = params.get("Color_By", "Annotation") - point_size = params.get("Dot_Size", 1) - v_min = params.get("Value_Min", "None") - v_max = params.get("Value_Max", "None") - - feature = text_to_value(feature) - annotation = text_to_value(annotation) - - if color_by == "Annotation": - feature = None - else: - annotation = None - - # Store the original value of layer - layer_input = layer - - layer = text_to_value(layer, default_none_text="Original") - - vmin = text_to_value( - v_min, - default_none_text="None", - value_to_convert_to=None, - to_float=True, - param_name="Value Min" - ) - - vmax = text_to_value( - v_max, - default_none_text="None", - value_to_convert_to=None, - to_float=True, - param_name="Value Max" - ) - - plt.rcParams.update({'font.size': font_size}) - - fig, ax = dimensionality_reduction_plot( - adata=adata, - method=method, - annotation=annotation, - feature=feature, - layer=layer, - point_size=point_size, - vmin=vmin, - vmax=vmax - ) - - if color_by == "Annotation": - title = annotation - else: - title = f'Table:"{layer_input}" \n Feature:"{feature}"' - ax.set_title(title) - - fig = ax.get_figure() - - fig.set_size_inches( - fig_width, - fig_height - ) - fig.set_dpi(fig_dpi) - - legend = ax.get_legend() - has_legend = legend is not None - - if has_legend: - ax.legend( - loc=legend_location, - bbox_to_anchor=(1, 0.5), - fontsize=legend_label_size, - markerscale=legend_marker_scale - ) - - plt.tight_layout() - - if show_plot: - plt.show() - - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "plots.png") - # Ensure proper extension - if not output_file.endswith(('.png', '.pdf', '.svg')): - output_file = output_file + '.png' - - fig.savefig(output_file, dpi=fig_dpi, bbox_inches='tight') - saved_files = {output_file: output_file} - - print( - f"UMAP\\tSNE\\PCA Visualization completed → " - f"{saved_files[output_file]}" - ) - return saved_files - else: - # Return the figure and dataframe directly for in-memory workflows - print("Returning figure (not saving to file)") - # Note: This template doesn't produce a dataframe, just a figure - return fig, None - - -# CLI interface -if __name__ == "__main__": - if len(sys.argv) != 2: - print( - "Usage: python umap_tsne_pca_template.py ", - file=sys.stderr - ) - sys.exit(1) - - result = run_from_json(sys.argv[1]) - - if isinstance(result, dict): - print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") - else: - print("\nReturned figure") \ No newline at end of file diff --git a/src/spac/templates/visualize_ripley_template.py b/src/spac/templates/visualize_ripley_template.py deleted file mode 100644 index 01036bb6..00000000 --- a/src/spac/templates/visualize_ripley_template.py +++ /dev/null @@ -1,121 +0,0 @@ -""" -Platform-agnostic Visualize Ripley L template converted from NIDAP. -Maintains the exact logic from the NIDAP template. - -Usage ------ ->>> from spac.templates.visualize_ripley_template import run_from_json ->>> run_from_json("examples/visualize_ripley_params.json") -""" -import json -import sys -from pathlib import Path -from typing import Any, Dict, Union, List, Optional, Tuple -import pandas as pd -import matplotlib.pyplot as plt - -# Add parent directory to path for imports -sys.path.append(str(Path(__file__).parent.parent.parent)) - -from spac.visualization import plot_ripley_l -from spac.templates.template_utils import ( - load_input, - save_outputs, - parse_params, - text_to_value, -) - - -def run_from_json( - json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True, - show_plot: bool = True -) -> Union[Dict[str, str], Tuple[Any, pd.DataFrame]]: - """ - Execute Visualize Ripley L analysis with parameters from JSON. - Replicates the NIDAP template functionality exactly. - - Parameters - ---------- - json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the figure and - dataframe directly for in-memory workflows. Default is True. - show_plot : bool, optional - Whether to display the plot. Default is True. - - Returns - ------- - dict or tuple - If save_results=True: Dictionary of saved file paths - If save_results=False: Tuple of (figure, dataframe) - """ - # Parse parameters from JSON - params = parse_params(json_path) - - # Load the upstream analysis data - adata = load_input(params["Upstream_Analysis"]) - - # Extract parameters - center_phenotype = params["Center_Phenotype"] - neighbor_phenotype = params["Neighbor_Phenotype"] - plot_specific_regions = params.get("Plot_Specific_Regions", False) - regions_labels = params.get("Regions_Labels", []) - plot_simulations = params.get("Plot_Simulations", True) - - print(f"running with center_phenotype: {center_phenotype}, neighbor_phenotype: {neighbor_phenotype}") - - # Process regions parameter exactly as in NIDAP template - if plot_specific_regions: - if len(regions_labels) == 0: - raise ValueError( - 'Please identify at least one region in the ' - '"Regions Label(s) parameter' - ) - else: - regions_labels = None - - # Run the visualization exactly as in NIDAP template - fig, plots_df = plot_ripley_l( - adata, - phenotypes=(center_phenotype, neighbor_phenotype), - regions=regions_labels, - sims=plot_simulations, - return_df=True - ) - - if show_plot: - plt.show() - - # Print the dataframe to console - print(plots_df.to_string()) - - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "plots.csv") - saved_files = save_outputs({output_file: plots_df}) - - print(f"Visualize Ripley L completed → {saved_files[output_file]}") - return saved_files - else: - # Return the figure and dataframe directly for in-memory workflows - print("Returning figure and dataframe (not saving to file)") - return fig, plots_df - - -# CLI interface -if __name__ == "__main__": - if len(sys.argv) != 2: - print("Usage: python visualize_ripley_template.py ") - sys.exit(1) - - result = run_from_json(sys.argv[1]) - - if isinstance(result, dict): - print("\nOutput files:") - for filename, filepath in result.items(): - print(f" {filename}: {filepath}") - else: - print("\nReturned figure and dataframe") diff --git a/src/spac/templates/zscore_normalization_template.py b/src/spac/templates/zscore_normalization_template.py deleted file mode 100644 index 0c8b17da..00000000 --- a/src/spac/templates/zscore_normalization_template.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -Platform-agnostic Z-Score Normalization template converted from NIDAP. -Maintains the exact logic from the NIDAP template. - -Usage ------ ->>> from spac.templates.zscore_normalization_template import run_from_json ->>> run_from_json("examples/zscore_normalization_params.json") -""" -import json -import sys -from pathlib import Path -from typing import Any, Dict, Union -import pandas as pd -import pickle - -# Add parent directory to path for imports -sys.path.append(str(Path(__file__).parent.parent.parent)) - -from spac.transformations import z_score_normalization -from spac.templates.template_utils import ( - load_input, - save_outputs, - parse_params, -) - - -def run_from_json( - json_path: Union[str, Path, Dict[str, Any]], - save_results: bool = True -) -> Union[Dict[str, str], Any]: - """ - Execute Z-Score Normalization analysis with parameters from JSON. - Replicates the NIDAP template functionality exactly. - - Parameters - ---------- - json_path : str, Path, or dict - Path to JSON file, JSON string, or parameter dictionary - save_results : bool, optional - Whether to save results to file. If False, returns the adata object - directly for in-memory workflows. Default is True. - - Returns - ------- - dict or AnnData - If save_results=True: Dictionary of saved file paths - If save_results=False: The processed AnnData object - """ - # Parse parameters from JSON - params = parse_params(json_path) - - # Load the upstream analysis data - adata = load_input(params["Upstream_Analysis"]) - - # Extract parameters - input_layer = params["Table_to_Process"] - output_layer = params["Output_Table_Name"] - - if input_layer == "Original": - input_layer = None - - z_score_normalization( - adata, - output_layer=output_layer, - input_layer=input_layer - ) - - # Convert the normalized layer to a DataFrame and print its summary - post_dataframe = adata.to_df(layer=output_layer) - print(post_dataframe.describe()) - - print(adata) - - # Handle results based on save_results flag - if save_results: - # Save outputs - output_file = params.get("Output_File", "transform_output.pickle") - # Default to pickle format if no recognized extension - if not output_file.endswith(('.pickle', '.pkl', '.h5ad')): - output_file = output_file + '.pickle' - - saved_files = save_outputs({output_file: adata}) - - print(f"Z-Score Normalization completed → {saved_files[output_file]}") - return saved_files - else: - # Return the adata object directly for in-memory workflows - print("Returning AnnData object (not saving to file)") - return adata - - -# CLI interface -if __name__ == "__main__": - if len(sys.argv) != 2: - print( - "Usage: python zscore_normalization_template.py ", - file=sys.stderr - ) - sys.exit(1) - - saved_files = run_from_json(sys.argv[1]) - - if isinstance(saved_files, dict): - print("\nOutput files:") - for filename, filepath in saved_files.items(): - print(f" {filename}: {filepath}") - else: - print("\nReturned AnnData object") \ No newline at end of file From 8c7badc053f845896e306d09658aa0d1b215baf9 Mon Sep 17 00:00:00 2001 From: fangliu117 <> Date: Fri, 27 Feb 2026 20:09:44 +0000 Subject: [PATCH 102/102] ci(version): Automatic development release --- CHANGELOG.md | 344 ++++++++++++++++++++++++++++++++++++++++++- setup.py | 2 +- src/spac/__init__.py | 2 +- 3 files changed, 341 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b99276e2..395f9d26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,341 @@ # CHANGELOG -## v0.9.0 (2025-05-23) +## v0.9.1 (2026-02-27) -### Step +### Bug Fixes -- Bumping minor version - ([`e333641`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/e3336417a09b4ef26e71bde1b54da840f0980ab9)) +- Add missing 'import os' in performance tests + ([`7a5ec6d`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/7a5ec6d57ca7934d7d1003d417b3907f6d692308)) + +- test_boxplot_performance.py - test_histogram_performance.py + +- Add missing __init__.py in tests/templates + ([`7b7e6cb`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/7b7e6cbbe3bf9ea591de2075cb40861c2136a879)) + +- Remove 6 deprecated templates (sync with tools_refactor) + ([`d0bbc5e`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/d0bbc5ea8a7151bbf5f389549612b01862d6f382)) + +- Spac_boxplot outputs in json and validated ha5d + ([`c87e782`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/c87e782ff03c3ec52a2ae4c4353f5b426a6fc9d0)) + +- **boxplot**: Replace deprecated append call with concat + ([`4906439`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/4906439dfdbec132ba675e4d13b9c48cd33d8c38)) + +- **boxplot_template**: Address minor comments from copilot + ([`cd5abd0`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/cd5abd005d35678351867ae14020ca9d57317e02)) + +- **check_layer**: Use check_table spac function to evaluate if adata.layer is present + ([`0cf530b`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/0cf530bb603c0d74beeaa797df5f8ad222512921)) + +- **combine_annotations_template**: Address comments from copilot CR for + combine_annotations_template function + ([`9d8582a`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/9d8582a88b1d61cd1912aa146153178d8287d82a)) + +- **histogram_performance**: Add clarifying comment for old hist implementation + ([`179482e`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/179482eb16bbfaa7566bfcc8aae0adb29c0d2429)) + +- **histogram_template**: Fix odd number of cells in test + ([`51ba1c4`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/51ba1c4609e5d4435e612faf6b632bd8f1f76927)) + +- **interactive_spatial_plot_template**: Remove nidap comments + ([`64ff302`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/64ff3023155345b86ad9b72838bae0b53db21930)) + +- **nearest_neighbor_template**: Break the title in two lines + ([`4e083fb`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/4e083fbe77a1aaa1dac1d5b3d7841ed172721132)) + +- **normalize_batch_template**: Fix typo and unused import + ([`9edf8ce`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/9edf8cee2972ecfd34244d7f3482a7ca5be94b2e)) + +- **performance_test**: Fix the speedup calculation logic + ([`c31c3ff`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/c31c3ffec708bf2d2cf0a9ce487f54ccd04fe874)) + +- **posit_it_python_template**: Fixed typo + ([`e70f547`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/e70f547aae17a011ce52162c0bf6fd42a74902ed)) + +- **quantile_scaling_template**: Fix typo in both function and unit tests + ([`de6ee91`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/de6ee910c5a36eb7fa2c6429f36695317ceebb03)) + +- **relational_heatmap_template**: Address the issue of insecure temporary file and comments from + copilot + ([`5662bdb`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/5662bdbf77980af891bd55e45c02a20ed7af7546)) + +- **ripley_template**: Address review comments - merge dev into the branch and fix unit test + ([`415df89`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/415df89b0d3d368fa126b6167d2fb68028a8b512)) + +- **ripley_template**: Address review comments - replace debug prints with logging + ([`9914716`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/9914716db356d8ca217963a0d95c85bb37e2ff19)) + +- **sankey_plot_template**: Address the comments from copilot + ([`07baeb9`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/07baeb9037fd6bc3cefee2b71abb1b2d777223d9)) + +- **scripts**: Remove old performance testing script + ([`c0762c3`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/c0762c34c453e35bbe07e9a57180686cde1de7e8)) + +- **select_values_template**: Fix pandas/numpy version compatibility issue + ([`cedb6d1`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/cedb6d163d1ffc7383961c0291a20a474f35843f)) + +- **setup_analysis_template**: Fix setup_analysis_template function + ([`dddc33a`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/dddc33a78a0d6af0262eefd4b9250c0cfc19b77e)) + +- **spatial_interaction_template**: Fix typo + ([`1a3d03d`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/1a3d03daa6e0630132c2596e149a74cdba0520a0)) + +- **spatial_plot_temp**: Addrss copilot comments spatial_plot_template.py + ([`028a049`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/028a049bf5857cdb51eff4a76a4940dc50100872)) + +- **subset_analysis_template**: Fix typo and enhance function + ([`aefcb29`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/aefcb294f3d3ab5059e76232c667b5c3a37963a1)) + +- **summarize_dataframe_template**: Address comments from copilot + ([`30118f9`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/30118f9813cc8768401b2206ca1ba867a79175aa)) + +- **template_utils**: Address review comments + ([`e9f0883`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/e9f088335c76c093aa75edc81b3bab33b53a30c8)) + +- **template_utils**: Address review comments again + ([`c94b219`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/c94b219f30b551caeffa5074dda6173cf3a9ab8f)) + +- **template_utils**: Use applymap instead of map for pandas compatibility + ([`bbfa2f6`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/bbfa2f6cc82aadaf67b904642ec3f0ff61b3a816)) + +- **test_arcsinh_normalization_template**: Handle odd numbers with better list slicing + ([`afca7ff`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/afca7ff1eb153e3545a805a04be2df4acd283d15)) + +- **test_manual_phenotyping_temp**: Address comments of copilot review for unit tests + ([`e4d61cf`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/e4d61cf3748e6eb9f3aca7cad9faf2b41b6ea652)) + +- **test_performance**: Set the path to include spac + ([`9c4d606`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/9c4d606b9baf125dd5084125eb29e439b1930ab4)) + +- **tsne_analysis_template**: Fixed typo + ([`7ae8e57`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/7ae8e5725326e68e9f2ce440be62089c06ad5f36)) + +- **umap_transformation_template**: Return adata in place and fix comments of copilot + ([`9d24638`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/9d24638e5a235f3e128e72fc1e11f28f52bb1822)) + +- **umap_tsne_pca_template**: Address the comments from copilot + ([`7fb9b2c`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/7fb9b2cd291e3aa3651e5b3d01240772330609ff)) + +- **visualize_nearest_neighbor_template**: Fix typo + ([`52a4ee6`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/52a4ee6ef66f9ffaed59391bbe6d0fd4f003a816)) + +- **visualize_ripley**: Add missing __init__.py for templates module + ([`ff9238c`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/ff9238c126ab5e524608b24c28a47bebc3ed487d)) + +- **visualize_ripley**: Make plt.show() conditional based on show_plot parameter + ([`0e2747e`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/0e2747e8d05547c2e2dca4ad9e2b8ec730e24260)) + +### Code Style + +- **qc-metrics**: Fix spelling typo in nFeature metric + ([`59675ca`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/59675cad6540787be3a8a8a300dcc6c7e398dec9)) + +### Features + +- Add refactored galaxy tools + ([`4d2e3d7`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/4d2e3d722472e4a1808e936bd6267cc59e709a55)) + +- Add spac arcsinh_norm interactive_spatial_plot galaxy tools + ([`67e3ec4`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/67e3ec45822572f147f785999dfa0c4121d01635)) + +- Add SPAC boxplot Galaxy tool for Docker deployment + ([`9e3bea0`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/9e3bea0de6400b3a1031f831f8ced81f410b9007)) + +- Add spac_load_csv_files galaxy tools + ([`d2526a7`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/d2526a79965acb886c0100a04385f5325ec1923b)) + +- Add spac_setup_analysis galaxy tools + ([`bb6834b`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/bb6834bc2d292604f7499e09e9245384a3b0f694)) + +- Add spac_zscore_normalization galaxy tools + ([`cbbcd9e`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/cbbcd9e47930cf9b258a8668af6ec81238ad09d9)) + +- Refactor all templates and unit tests + ([`8005111`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/800511118ab5970754b4e09bf7017b423328da92)) + +- Refactored all template run_from_json() functions to use centralized save_results from + template_utils - Added show_static_image toggle (default False) to relational_heatmap_template and + sankey_plot_template to prevent Plotly-to-PNG hang on Galaxy - Refactored all unit tests in + tests/templates/ using snowball approach: real data, real filesystem, no mocking - One test file + per template validating output file existence, naming conventions, and non-empty artifacts - + Updated posit_it_python_template to use centralized save_results + +Templates changed: 43 files in src/spac/templates/ Tests changed: 37 files in tests/templates/ + +- **add_pin_color_rule_template**: Add add_pin_color_rule_template fnction and unit tests + ([`2477266`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/2477266b7de03e603b1dc9d48b9a53bed0af61ad)) + +- **analysis_to_csv_template**: Add analysis_to_csv_template function and unit tests + ([`448a980`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/448a980c496dfa5295a33e4432649947da6e6af7)) + +- **append_annotation_template**: Add append_annotation_template function and unit tests + ([`5e68e02`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/5e68e02faaa423f381d3d7321c1378f51e7f3c7f)) + +- **arcsinh_normalization_template**: Add arcsinh_normalization_template function and unit tests + ([`ff6cce4`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/ff6cce42b602642a4bd2f211d1dbd1fe6c6fd65e)) + +- **binary_to_categorical_annotation_template**: Add binary_to_categorical_annotation_template + function and unit tests + ([`8e500ec`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/8e500ecfe264b6125b6ace828003684e4b1b5cad)) +- **boxplot_template**: Add boxplot_template function and unit tests + ([`eb810ab`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/eb810ab4a532901817a5652ad59733af38b083fd)) -## v0.8.11 (2025-05-23) +- **calculate_centroid_template**: Add calculate_centroid_template function and unit tests + ([`4fea9c3`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/4fea9c336dabc207d6fa66de8553d3fe89c9dd62)) + +- **combine_annotations_template**: Add combine_annotations_template function and unit tests + ([`829a4bd`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/829a4bdbc95575079d9e24492f0e6bc2ea57475e)) + +- **combine_dataframes_template**: Add combine_dataframes_template function and unit tests + ([`3e24237`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/3e24237c5a4e3f57e09c0396532200dcdf471990)) + +- **downsample_cells_template**: Add downsample_cells_template function and unit tests + ([`47adf3e`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/47adf3e881f28d05b4627b1ec3d0954b403f634e)) + +- **hierarchical_heatmap_template**: Add hierarchical_heatmap_template and unit tests + ([`67e5a80`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/67e5a802500459e01a918c7fe96d20ab45c373c2)) + +- **hierarchical_heatmap_template**: Add hierarchical_heatmap_template function and unit tests + ([`6466e2f`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/6466e2f8daa4c95c49e0b6d0b0e5ec1460064d09)) + +- **histogram_template**: Add histogram_template and unit tests + ([`3380427`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/3380427ecbd6d743aaedfb954d612905153079e4)) + +- **interactive_spatial_plot_template**: Add interactive_spatial_plot_template function and unit + tests + ([`3f4336b`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/3f4336bc75f04ef1f4f8ca740a8b9b678a288da2)) + +- **load_csv**: Add load_csv template function with configuration support + ([`5456658`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/5456658e8d7b0ab0475ead65bbe982ccefbacf47)) + +- Add load_csv_files() to template_utils.py for loading and combining CSV files - Add + spell_out_special_characters() to handle biological marker names - Add + load_csv_files_with_config.py template wrapper for NIDAP compatibility - Add comprehensive unit + tests for both functions - Support column name cleaning, metadata mapping, and string column + enforcement + +- **manual_phenotyping_template**: Add manual_phenotyping_template function and unit tests + ([`941d641`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/941d641352c6eaee1c293b5ef98f1a9d92646c0c)) + +- **nearest_neighbor_calculation_template**: Add nearest_neighbor_calculation_template function and + unit tests + ([`19cd477`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/19cd477fbb3af575ae2b90615b34e801fb4dc66c)) + +- **neighborhood_profile_template**: Add neighborhood_profile_template function and unit tests + ([`824d131`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/824d131fed2216a5a291f61fc53d53e5e2c98c11)) + +- **normalize_batch_template**: Add normalize_batch_template functionand unit tests + ([`a71e865`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/a71e8656033f79736330a1f902200a6c81c4b37e)) + +- **phenograph_clustering_template**: Add phenograph_clustering_template function and unit tests + ([`ca29330`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/ca2933068c63c0a877970a496d4d9e2afe34d447)) + +- **posit_it_python_template**: Add posit_it_python_template functionand unit tests + ([`bbb53f7`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/bbb53f712285a19f738aa117205e907c1aa0404d)) + +- **qc-metrics**: Add common single cell quality control metrics + ([`994bac4`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/994bac4896650ecaeee7c81e2084872509a4b815)) + +- **qc_summary_statistics**: Add summary statistics table for sc/spatial transcriptomics quality + control metrics + ([`a228e5e`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/a228e5eb33777d03bec96af826568545e44157fd)) + +- **quantile_scaling_template**: Refactor nidap code, add quantile_scaling_template function and + unit tests + ([`542f985`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/542f985f4d1a2811f010b68da8dadffe8ac64220)) + +- **relational_heatmap_template**: Add relational_heatmap_template function and unit tests + ([`c57075d`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/c57075d87995b7f35b0c9065a354363f7cceb623)) + +- **rename_labels_template**: Add rename_labels_template function and unit tests + ([`96446d7`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/96446d70e392aa00e1ba77b2abddaa040d6aea7a)) + +- **ripley_l_template**: Add ripley_l_template and unit tests + ([`c889259`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/c88925913faf4c99c906e9b55a073759395faa78)) + +- **sankey_plot_template**: Add sankey_plot_template function and unit tests + ([`34b4eee`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/34b4eee45ae98fd0ce7c5f184611f4993125949f)) + +- **select_values_template**: Add select_values_template function and unit tests + ([`e59c994`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/e59c994352f09ee1398f9cea6cc02041daa0cd03)) + +- **setup_analysis_template**: Add setup_analysis_template function and unit tests + ([`1cfb39e`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/1cfb39ed56e30e805bf7bcf29399e8ce0f20333c)) + +- **spatial_interaction_template**: Add spatial_interaction_template and unit tests + ([`a7b1349`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/a7b13494e86b14555aba6a6fbcc0c7c12c1441f1)) + +- **spatial_plot_temp**: Add spatial_plot_template.py and unit tests + ([`0f26c08`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/0f26c08744f5611f0aa92a9a60b0ad20815e8d6e)) + +- **subset_analysis_template**: Add subset_analysis_template function and unit tests + ([`1db00a8`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/1db00a88dfdfbbc68862b3aac99aea5332e2255c)) + +- **summarize_annotation_statistics**: Add summarize_annotation_statistics template function and + unit tests + ([`34961bd`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/34961bde03bfc47211047ec58cbacc0814712e3c)) + +- **summarize_dataframe_template**: Add summarize_dataframe_template function and unit tests + ([`06d8feb`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/06d8feb4d72d132efb6c4db04f6254a8bd69ca04)) + +- **template_utils**: Add string_list_to_dictionary to template utils + ([`6ab7a9d`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/6ab7a9d91111c6fd1aa4a771bf85f8d07bd01b28)) + +- **template_utils**: Add template_utils and unit tests + ([`b960684`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/b960684f3e1887330f91ad307cc36b467d68bea3)) + +- **test_performance**: Add performance tests for boxplot/histogram + ([`862e523`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/862e523d08f40bb1e0aee53437fa06bdb533ac45)) + +- **tsne_analysis_template**: Add tsne_analysis_template function and unit tests + ([`abda610`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/abda61091b2a94251eccbe2535efc300d79a7e73)) + +- **umap_transformation_template**: Add umap_transformation_template function and unit tests + ([`e79fd78`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/e79fd7814a8728c9f9a529e90256ac44726d1571)) + +- **umap_tsne_pca_template**: Add umap_tsne_pca_template function and unit tests + ([`d67f6c7`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/d67f6c7278e7c8622ca43832acf8b74ae4a4363e)) + +- **utag_clustering_template**: Add utag_clustering_template and unit tests + ([`6da3985`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/6da39852b1d2ee8b3a1b4fda5c040055d6ae3cd4)) + +- **utag_clustering_template**: Add utag_clustering_template and unit tests + ([`743fb10`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/743fb10f71dc015b8892a4b291d3a2a9069e89a2)) + +- **visualize_nearest_neighbor_template**: Add visualize_nearest_neighbor_template function and unit + tests + ([`07ecdfa`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/07ecdfa15c3b5c4c7f65288811306bf68cef4962)) + +- **visualize_ripley_template**: Add visualize_ripley_template and unit tests + ([`48608e2`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/48608e26ca16c1e89a573286553b370f2a3f508b)) + +- **zscore_normalization_template**: Add zscore_normalization_template and unit tests + ([`b2d68c5`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/b2d68c5fb6cd1dfba38684d855f38aea98d56296)) + +### Refactoring + +- Merge paper.bib and paper.md updates from address-reviewer-comments branch + ([`9ae3ef3`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/9ae3ef331290197d3fcc305fb88f9b2baac3bccc)) + +- Streamline galaxy tools implementation + ([`009d010`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/009d01000c6a80dad9f6dc4a508b334a19796b3b)) + +- **get_qc_summary_table**: Adjust code style to adhere to spac guidlines closer + ([`5e03dc2`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/5e03dc23b5deaadb853fd26f327f643d7e19ad12)) + +- **get_qc_summary_table**: Refactor quality control summary statistics function and tests based on + the PR review + ([`d5061c4`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/d5061c43d31c20338576c58d207619f9ae789143)) + +### Testing + +- **perforamnce**: Skip performance tests by default + ([`fc664ad`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/fc664ad96b3375a3255f9bb36e51d0e4a505daba)) + + +## v0.9.0 (2025-05-23) ### Bug Fixes @@ -82,6 +408,9 @@ ### Continuous Integration +- **version**: Automatic development release + ([`3e126e9`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/3e126e9711be5d485010ced7460f99a180c8089e)) + - **version**: Automatic development release ([`195761d`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/195761de5563e80a60a7ea43ecb73e6105dc7d1d)) @@ -173,6 +502,11 @@ - **interactive_spatial_plot**: Used partial for better readability ([`60283bd`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/60283bd7671d2f2a65b52d77f4792b7461a8e407)) +### Step + +- Bumping minor version + ([`e333641`](https://github.com/FNLCR-DMAP/SCSAWorkflow/commit/e3336417a09b4ef26e71bde1b54da840f0980ab9)) + ### Testing - **comments**: Add extensive comments for complex data set generation in utag tests diff --git a/setup.py b/setup.py index 945475ad..79b1bbff 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='spac', - version="0.9.0", + version="0.9.1", description=( 'SPatial Analysis for single-Cell analysis (SPAC)' 'is a Scalable Python package for single-cell spatial protein data ' diff --git a/src/spac/__init__.py b/src/spac/__init__.py index f8b63dd6..c7a7ff09 100644 --- a/src/spac/__init__.py +++ b/src/spac/__init__.py @@ -22,7 +22,7 @@ functions.extend(module_functions) # Define the package version before using it in __all__ -__version__ = "0.9.0" +__version__ = "0.9.1" # Define a __all__ list to specify which functions should be considered public __all__ = functions

V?yxA6gm9bwOm? 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 z%0D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

opmwFtZ6C)%n1A8qqI>A$WXCh$Oa6$L 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 ?z_U83T^;AT>5 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≠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 z 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