citegeist 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Alexander Chang and Brent T. Schlegel
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,199 @@
1
+ Metadata-Version: 2.2
2
+ Name: citegeist
3
+ Version: 0.1.0
4
+ Summary: A spatial transcriptomics deconvolution tool for cell type identification and gene expression analysis
5
+ Home-page: https://github.com/acc383/CITEgeist
6
+ Author: Alexander Chang, Brent T. Schlegel
7
+ Author-email: acc383@cornell.edu
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: License :: OSI Approved :: BSD License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: numpy>=1.20.0
20
+ Requires-Dist: pandas>=1.3.0
21
+ Requires-Dist: scanpy>=1.9.0
22
+ Requires-Dist: scipy>=1.7.0
23
+ Requires-Dist: pyarrow>=5.0.0
24
+ Requires-Dist: gurobipy>=9.5.0
25
+ Requires-Dist: squidpy>=1.6.2
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
28
+ Requires-Dist: pytest-cov>=3.0.0; extra == "dev"
29
+ Requires-Dist: black>=22.0.0; extra == "dev"
30
+ Requires-Dist: isort>=5.10.0; extra == "dev"
31
+ Dynamic: author
32
+ Dynamic: author-email
33
+ Dynamic: classifier
34
+ Dynamic: description
35
+ Dynamic: description-content-type
36
+ Dynamic: home-page
37
+ Dynamic: provides-extra
38
+ Dynamic: requires-dist
39
+ Dynamic: requires-python
40
+ Dynamic: summary
41
+
42
+ # CITEgeist: Cellular Indexing of Transcriptomes and Epitopes for Guided Exploration of Intrinsic Spatial Trends
43
+
44
+ CITEgeist is a computational method for deconvolving spatial transcriptomics data using spatially-resolved CITE-seq measurements. The pipeline performs both cell-type proportion estimation and gene expression deconvolution in a two-pass approach, leveraging both protein and RNA measurements from the same spatial locations.
45
+
46
+ ## Quick Installation
47
+
48
+ You can now also install CITEgeist using pip:
49
+
50
+ ```bash
51
+ pip install citegeist
52
+ ```
53
+
54
+ ## Table of Contents
55
+ 1. [System Requirements](#system-requirements)
56
+ - [Software Dependencies](#software-dependencies)
57
+ - [Hardware Requirements](#hardware-requirements)
58
+ 2. [Getting Started](#getting-started)
59
+ - [1. Download the Code and Data](#1-download-the-code-and-data)
60
+ - [2. Set Up the Environment](#2-set-up-the-environment)
61
+ - [3. Obtain Gurobi License](#3-obtain-gurobi-license)
62
+ - [4. Running CITEgeist](#4-running-citegeist)
63
+ 3. [Benchmarking and Reproducibility](#benchmarking-and-reproducibility)
64
+ 4. [Run the Analysis](#run-the-analysis)
65
+
66
+ ## System Requirements
67
+
68
+ ### Software Dependencies
69
+
70
+ - **Operating System**:
71
+ - Linux
72
+ - macOS
73
+ - Windows 10 with WSL2
74
+
75
+ - **Python**: 3.10
76
+ - **Gurobi** [version > 3.9](https://www.gurobi.com/downloads/gurobi-software/)
77
+ #### Key Python Dependencies
78
+ - scanpy==1.10.4
79
+ - anndata==0.11.3
80
+ - numpy==1.26.4
81
+ - pandas==2.2.3
82
+ - scipy==1.13.1
83
+ - scikit-learn==1.6.1
84
+ - gurobipy==11.0.2 (requires license)
85
+ - matplotlib==3.10.0
86
+ - seaborn==0.13.2
87
+ - h5py==3.12.1
88
+ - squidpy==1.6.2
89
+ - spatialdata==0.2.5.post0
90
+
91
+ It is recommended to install the dependencies in the `CITEgeist_env.yml` file for running the notebooks.
92
+
93
+ ### Hardware Requirements
94
+ - **RAM**: Minimum 16GB, Recommended 64GB+
95
+ - **Storage**: 16GB minimum for installation and basic analysis
96
+ - **CPU**: Multi-core processor recommended (8+ cores for optimal performance)
97
+
98
+ ---
99
+
100
+ ## Getting Started
101
+
102
+ ### 1. Download the Code and Data (Instructions for Peer Reviewers)
103
+
104
+ 1. Download the code from Figshare: [https://figshare.com/s/34e456fd7786e5211acc](https://figshare.com/s/34e456fd7786e5211acc)
105
+ 2. Unzip the downloaded file to your preferred location.
106
+ 3. Download the data from GEO (Reviewers: see Data Availability section in the manuscript for the GEO link and Private Access Token).
107
+ 4. Run the following code to strip unique identifiers required by GEO:
108
+
109
+ ```bash
110
+ # go to the 'data' directory
111
+ cd data
112
+
113
+ # untar the raw files
114
+ mkdir -pv ./GEO_data
115
+ tar -xvf GEO_data_RAW.tar -C ./GEO_data
116
+
117
+ # run the py preprocessing script
118
+ ## round 1) aggregate the files by sample
119
+ python3 ./delete_all_but_essential.py --folder GEO_data # select option: 1
120
+
121
+ ## round 2) remove the prefix from necessary files
122
+ python3 ./delete_all_but_essential.py --folder GEO_data # select option: 2
123
+ ```
124
+
125
+ Note: When prompted, select Option 1 or 2 and type 'Yes' to confirm.
126
+
127
+ ### 2. Set Up the Environment
128
+
129
+ - Install dependencies using the provided environment file:
130
+
131
+ ```bash
132
+ conda env create -f CITEgeist_env.yml
133
+ ```
134
+
135
+ - Activate the environment and set up a Jupyter kernel:
136
+
137
+ ```bash
138
+ conda activate CITEgeist_env
139
+ ```
140
+
141
+ ### 3. Obtain Gurobi License
142
+
143
+ CITEgeist requires a Gurobi license (free for academic use):
144
+
145
+ 1. Sign up for an academic license at: [https://www.gurobi.com/downloads/end-user-license-agreement-academic/](https://www.gurobi.com/downloads/end-user-license-agreement-academic/)
146
+ 2. Follow the instructions to download and install your license.
147
+ 3. Update the license file path in the notebooks to match your local license location.
148
+
149
+ ### 4. Running CITEgeist
150
+
151
+ You can run CITEgeist in two ways:
152
+
153
+ #### A. Using Jupyter Notebooks
154
+ - Update data paths in the top of the notebooks to match your local directory structure.
155
+ - Expected runtime on a standard computer (16 threads, 32GB RAM):
156
+ - Vignette 1: ~2 hours
157
+ - Vignette 2: ~2 hours
158
+ - Vignette 3: ~10 hours
159
+
160
+ #### Key Parameters:
161
+ - `radius`: Radius for neighbor detection (default: 4)
162
+ - `lambda_reg`: Regularization strength for cell proportion estimation (default: 0.001)
163
+ - `alpha_elastic`: Elastic net mixing parameter for cell proportion estimation (default: 0.7)
164
+ - `max_y_change`: Maximum allowed change in Y values (default: 0.2)
165
+
166
+ #### Optional Parameters:
167
+ - `profiling_only`: Set for cell-type proportions only.
168
+ - `max_workers`: Number of parallel workers.
169
+ - `checkpoint_interval`: Checkpoint saving interval.
170
+
171
+ #### B. Using SLURM Distribution
172
+ For large-scale analyses, you can use the provided `CITEgeist/examples/sbatch_sample.sh` script for distributed computing.
173
+
174
+ ---
175
+
176
+ ## Benchmarking and Reproducibility
177
+
178
+ For specific reproduction of benchmarking tests and detailed methodology, please refer to the 'examples' and 'benchmarking' section in the documentation.
179
+
180
+ ---
181
+
182
+ ## Run the Analysis
183
+
184
+ You can either:
185
+
186
+ #### A. Run the notebooks directly:
187
+ - Update data paths in the notebooks to match your local directory structure.
188
+ - Expected runtime: ~2 hours on a standard computer (16 threads, 32GB RAM).
189
+
190
+ #### B. Use SLURM distribution:
191
+ - Use the provided `examples/sbatch_sample.sh` script for distributed computing.
192
+
193
+ ---
194
+
195
+ ## Additional System Requirements
196
+ - **RAM**: 32GB (minimum)
197
+ - **CPU**: 16 threads (recommended)
198
+ - **Storage**: Sufficient space for the GEO dataset
199
+ - **Operating System**: Linux/Unix recommended (Windows users may need additional configuration)
@@ -0,0 +1,158 @@
1
+ # CITEgeist: Cellular Indexing of Transcriptomes and Epitopes for Guided Exploration of Intrinsic Spatial Trends
2
+
3
+ CITEgeist is a computational method for deconvolving spatial transcriptomics data using spatially-resolved CITE-seq measurements. The pipeline performs both cell-type proportion estimation and gene expression deconvolution in a two-pass approach, leveraging both protein and RNA measurements from the same spatial locations.
4
+
5
+ ## Quick Installation
6
+
7
+ You can now also install CITEgeist using pip:
8
+
9
+ ```bash
10
+ pip install citegeist
11
+ ```
12
+
13
+ ## Table of Contents
14
+ 1. [System Requirements](#system-requirements)
15
+ - [Software Dependencies](#software-dependencies)
16
+ - [Hardware Requirements](#hardware-requirements)
17
+ 2. [Getting Started](#getting-started)
18
+ - [1. Download the Code and Data](#1-download-the-code-and-data)
19
+ - [2. Set Up the Environment](#2-set-up-the-environment)
20
+ - [3. Obtain Gurobi License](#3-obtain-gurobi-license)
21
+ - [4. Running CITEgeist](#4-running-citegeist)
22
+ 3. [Benchmarking and Reproducibility](#benchmarking-and-reproducibility)
23
+ 4. [Run the Analysis](#run-the-analysis)
24
+
25
+ ## System Requirements
26
+
27
+ ### Software Dependencies
28
+
29
+ - **Operating System**:
30
+ - Linux
31
+ - macOS
32
+ - Windows 10 with WSL2
33
+
34
+ - **Python**: 3.10
35
+ - **Gurobi** [version > 3.9](https://www.gurobi.com/downloads/gurobi-software/)
36
+ #### Key Python Dependencies
37
+ - scanpy==1.10.4
38
+ - anndata==0.11.3
39
+ - numpy==1.26.4
40
+ - pandas==2.2.3
41
+ - scipy==1.13.1
42
+ - scikit-learn==1.6.1
43
+ - gurobipy==11.0.2 (requires license)
44
+ - matplotlib==3.10.0
45
+ - seaborn==0.13.2
46
+ - h5py==3.12.1
47
+ - squidpy==1.6.2
48
+ - spatialdata==0.2.5.post0
49
+
50
+ It is recommended to install the dependencies in the `CITEgeist_env.yml` file for running the notebooks.
51
+
52
+ ### Hardware Requirements
53
+ - **RAM**: Minimum 16GB, Recommended 64GB+
54
+ - **Storage**: 16GB minimum for installation and basic analysis
55
+ - **CPU**: Multi-core processor recommended (8+ cores for optimal performance)
56
+
57
+ ---
58
+
59
+ ## Getting Started
60
+
61
+ ### 1. Download the Code and Data (Instructions for Peer Reviewers)
62
+
63
+ 1. Download the code from Figshare: [https://figshare.com/s/34e456fd7786e5211acc](https://figshare.com/s/34e456fd7786e5211acc)
64
+ 2. Unzip the downloaded file to your preferred location.
65
+ 3. Download the data from GEO (Reviewers: see Data Availability section in the manuscript for the GEO link and Private Access Token).
66
+ 4. Run the following code to strip unique identifiers required by GEO:
67
+
68
+ ```bash
69
+ # go to the 'data' directory
70
+ cd data
71
+
72
+ # untar the raw files
73
+ mkdir -pv ./GEO_data
74
+ tar -xvf GEO_data_RAW.tar -C ./GEO_data
75
+
76
+ # run the py preprocessing script
77
+ ## round 1) aggregate the files by sample
78
+ python3 ./delete_all_but_essential.py --folder GEO_data # select option: 1
79
+
80
+ ## round 2) remove the prefix from necessary files
81
+ python3 ./delete_all_but_essential.py --folder GEO_data # select option: 2
82
+ ```
83
+
84
+ Note: When prompted, select Option 1 or 2 and type 'Yes' to confirm.
85
+
86
+ ### 2. Set Up the Environment
87
+
88
+ - Install dependencies using the provided environment file:
89
+
90
+ ```bash
91
+ conda env create -f CITEgeist_env.yml
92
+ ```
93
+
94
+ - Activate the environment and set up a Jupyter kernel:
95
+
96
+ ```bash
97
+ conda activate CITEgeist_env
98
+ ```
99
+
100
+ ### 3. Obtain Gurobi License
101
+
102
+ CITEgeist requires a Gurobi license (free for academic use):
103
+
104
+ 1. Sign up for an academic license at: [https://www.gurobi.com/downloads/end-user-license-agreement-academic/](https://www.gurobi.com/downloads/end-user-license-agreement-academic/)
105
+ 2. Follow the instructions to download and install your license.
106
+ 3. Update the license file path in the notebooks to match your local license location.
107
+
108
+ ### 4. Running CITEgeist
109
+
110
+ You can run CITEgeist in two ways:
111
+
112
+ #### A. Using Jupyter Notebooks
113
+ - Update data paths in the top of the notebooks to match your local directory structure.
114
+ - Expected runtime on a standard computer (16 threads, 32GB RAM):
115
+ - Vignette 1: ~2 hours
116
+ - Vignette 2: ~2 hours
117
+ - Vignette 3: ~10 hours
118
+
119
+ #### Key Parameters:
120
+ - `radius`: Radius for neighbor detection (default: 4)
121
+ - `lambda_reg`: Regularization strength for cell proportion estimation (default: 0.001)
122
+ - `alpha_elastic`: Elastic net mixing parameter for cell proportion estimation (default: 0.7)
123
+ - `max_y_change`: Maximum allowed change in Y values (default: 0.2)
124
+
125
+ #### Optional Parameters:
126
+ - `profiling_only`: Set for cell-type proportions only.
127
+ - `max_workers`: Number of parallel workers.
128
+ - `checkpoint_interval`: Checkpoint saving interval.
129
+
130
+ #### B. Using SLURM Distribution
131
+ For large-scale analyses, you can use the provided `CITEgeist/examples/sbatch_sample.sh` script for distributed computing.
132
+
133
+ ---
134
+
135
+ ## Benchmarking and Reproducibility
136
+
137
+ For specific reproduction of benchmarking tests and detailed methodology, please refer to the 'examples' and 'benchmarking' section in the documentation.
138
+
139
+ ---
140
+
141
+ ## Run the Analysis
142
+
143
+ You can either:
144
+
145
+ #### A. Run the notebooks directly:
146
+ - Update data paths in the notebooks to match your local directory structure.
147
+ - Expected runtime: ~2 hours on a standard computer (16 threads, 32GB RAM).
148
+
149
+ #### B. Use SLURM distribution:
150
+ - Use the provided `examples/sbatch_sample.sh` script for distributed computing.
151
+
152
+ ---
153
+
154
+ ## Additional System Requirements
155
+ - **RAM**: 32GB (minimum)
156
+ - **CPU**: 16 threads (recommended)
157
+ - **Storage**: Sufficient space for the GEO dataset
158
+ - **Operating System**: Linux/Unix recommended (Windows users may need additional configuration)
File without changes
@@ -0,0 +1,21 @@
1
+ # Expose key classes and functions for easy access
2
+
3
+ from .citegeist_model import CitegeistModel
4
+ from .gurobi_impl import optimize_cell_proportions, optimize_gene_expression, map_antibodies_to_profiles
5
+ from .utils import (
6
+ save_results_to_output,
7
+ validate_cell_profile_dict,
8
+ cleanup_memory,
9
+ setup_logging
10
+ )
11
+
12
+ __all__ = [
13
+ "CitegeistModel",
14
+ "optimize_cell_proportions",
15
+ "optimize_gene_expression",
16
+ "map_antibodies_to_profiles",
17
+ "save_results_to_output",
18
+ "validate_cell_profile_dict",
19
+ "cleanup_memory",
20
+ "setup_logging"
21
+ ]
@@ -0,0 +1,42 @@
1
+
2
+ import scanpy as sc
3
+ import anndata as ad
4
+
5
+
6
+ def filter_spots_by_celltype_proportion(adata, celltype, proportion_threshold):
7
+ """
8
+ Filter spots by celltype proportion based on if their .obs[celltype] is greater than the proportion_threshold.
9
+ """
10
+ return adata[adata.obs[celltype] > proportion_threshold].copy()
11
+
12
+
13
+ def expand_prop_gex_adata(prop_gex_adata, celltype_cutoff=0.2, celltype_profile_dict=None):
14
+ """
15
+ Expands celltype level spots to have multiple spots for each celltype, but retain spatial information
16
+ for COMMMOT analysis.
17
+ """
18
+
19
+ if celltype_profile_dict is None:
20
+ raise ValueError("celltype_profile_dict is required for this function.")
21
+ celltypes = celltype_profile_dict.keys()
22
+
23
+ all_adatas = []
24
+
25
+ for celltype in celltypes:
26
+ filtered_adata = filter_spots_by_celltype_proportion(prop_gex_adata, celltype, celltype_cutoff)
27
+ celltype_label = celltype.replace(" ", "_")
28
+ layer_name = f"{celltype_label}_genes_pass1"
29
+ filtered_adata.X = filtered_adata.layers[layer_name]
30
+ filtered_adata.obs['celltype'] = celltype
31
+ all_adatas.append(filtered_adata)
32
+ # Extract the corresponding layer for the filtered_adata, including the spatial information
33
+ combined_adata = ad.concat(all_adatas, join="outer", keys=celltypes, index_unique = "_", merge="same", uns_merge = "first")
34
+ return combined_adata
35
+
36
+ def get_celltype_expression_data(adata, celltype):
37
+ """
38
+ Get expression data for a specific celltype.
39
+ """
40
+ return adata[adata.obs['celltype'] == celltype].copy()
41
+
42
+
@@ -0,0 +1,174 @@
1
+ import os
2
+ import logging
3
+ import numpy as np
4
+ from pathlib import Path
5
+
6
+ class CheckpointManager:
7
+ """Manages loading and saving of optimization checkpoints."""
8
+
9
+ def __init__(self, output_dir, sample_name):
10
+ """
11
+ Initialize checkpoint manager.
12
+
13
+ Args:
14
+ output_dir (str): Directory for checkpoint files
15
+ sample_name (str): Unique identifier for this sample
16
+ """
17
+ self.output_dir = Path(output_dir)
18
+ self.sample_name = sample_name
19
+ self.output_dir.mkdir(parents=True, exist_ok=True)
20
+
21
+ def check_complete_run(self, N, T, M):
22
+ """
23
+ Check if a complete run exists.
24
+
25
+ Args:
26
+ N (int): Number of spots
27
+ T (int): Number of cell types
28
+ M (int): Number of genes/features
29
+
30
+ Returns:
31
+ dict or None: Dictionary of spot profiles if complete run exists, else None
32
+ """
33
+ complete_file = self.output_dir / f"{self.sample_name}_gene_expression_complete.npz"
34
+
35
+ if complete_file.exists():
36
+ try:
37
+ complete_data = np.load(complete_file)
38
+ if 'profiles' in complete_data and 'completed_spots' in complete_data:
39
+ profiles = complete_data['profiles']
40
+ if profiles.shape == (N, T, M):
41
+ return {i: profiles[i] for i in range(N)}
42
+ except Exception as e:
43
+ logging.error(f"Error loading complete file: {e}")
44
+ self._cleanup_corrupted_files()
45
+ return None
46
+
47
+ def load_latest_checkpoint(self, N, T, M):
48
+ """
49
+ Load the latest valid checkpoint.
50
+
51
+ Args:
52
+ N (int): Number of spots
53
+ T (int): Number of cell types
54
+ M (int): Number of genes/features
55
+
56
+ Returns:
57
+ tuple: (completed_spots set, spotwise_profiles dict)
58
+ """
59
+ checkpoints = list(self.output_dir.glob(
60
+ f"{self.sample_name}_gene_expression_checkpoint_*.npz"
61
+ ))
62
+
63
+ if not checkpoints:
64
+ return set(), {}
65
+
66
+ checkpoint_numbers = [
67
+ int(f.stem.split('_')[-1])
68
+ for f in checkpoints
69
+ ]
70
+ latest_number = max(checkpoint_numbers)
71
+ latest_checkpoint = self.output_dir / f"{self.sample_name}_gene_expression_checkpoint_{latest_number}.npz"
72
+
73
+ try:
74
+ checkpoint_data = np.load(latest_checkpoint)
75
+ if 'profiles' in checkpoint_data and 'completed_spots' in checkpoint_data:
76
+ profiles = checkpoint_data['profiles']
77
+ completed_spots = set(checkpoint_data['completed_spots'].tolist())
78
+
79
+ if profiles.shape == (N, T, M):
80
+ spotwise_profiles = {
81
+ i: profiles[i]
82
+ for i in completed_spots
83
+ if not np.any(np.isnan(profiles[i]))
84
+ }
85
+ completed_spots = set(spotwise_profiles.keys())
86
+ logging.info(f"Loaded {len(completed_spots)} valid profiles from checkpoint")
87
+ return completed_spots, spotwise_profiles
88
+
89
+ except Exception as e:
90
+ logging.error(f"Error loading checkpoint: {e}")
91
+ self._cleanup_corrupted_files()
92
+
93
+ return set(), {}
94
+
95
+ def save_checkpoint(self, completed_spots, spotwise_profiles, N, T, M):
96
+ """
97
+ Save current progress as checkpoint.
98
+
99
+ Args:
100
+ completed_spots (set): Set of completed spot indices
101
+ spotwise_profiles (dict): Dictionary of spot profiles
102
+ N (int): Number of spots
103
+ T (int): Number of cell types
104
+ M (int): Number of genes/features
105
+ """
106
+ try:
107
+ n_completed = len(completed_spots)
108
+ checkpoint_path = self.output_dir / f"{self.sample_name}_gene_expression_checkpoint_{n_completed}.npz"
109
+
110
+ # Convert dictionary to numpy array
111
+ max_spot = max(spotwise_profiles.keys())
112
+ profiles_array = np.full((max_spot + 1, T, M), np.nan)
113
+
114
+ for spot_idx, profile in spotwise_profiles.items():
115
+ profiles_array[spot_idx] = profile
116
+
117
+ # Save checkpoint
118
+ np.savez_compressed(
119
+ checkpoint_path,
120
+ profiles=profiles_array,
121
+ completed_spots=np.array(list(completed_spots)),
122
+ n_completed=n_completed
123
+ )
124
+
125
+ # Cleanup old checkpoints
126
+ self._cleanup_old_checkpoints(checkpoint_path)
127
+
128
+ logging.info(f"Saved checkpoint after {n_completed} completed spots")
129
+
130
+ except Exception as e:
131
+ logging.error(f"Failed to save checkpoint: {e}")
132
+
133
+ def save_final_results(self, spotwise_profiles, completed_spots, N, T, M):
134
+ """
135
+ Save final results.
136
+
137
+ Args:
138
+ spotwise_profiles (dict): Dictionary of spot profiles
139
+ completed_spots (set): Set of completed spot indices
140
+ N (int): Number of spots
141
+ T (int): Number of cell types
142
+ M (int): Number of genes/features
143
+ """
144
+ final_path = self.output_dir / f"{self.sample_name}_gene_expression_complete.npz"
145
+ max_spot = max(spotwise_profiles.keys())
146
+ final_profiles = np.full((max_spot + 1, T, M), np.nan)
147
+
148
+ for spot_idx, profile in spotwise_profiles.items():
149
+ final_profiles[spot_idx] = profile
150
+
151
+ np.savez_compressed(
152
+ final_path,
153
+ profiles=final_profiles,
154
+ completed_spots=np.array(list(completed_spots))
155
+ )
156
+ logging.info(f"Saved final results with {len(completed_spots)} completed spots")
157
+
158
+ def _cleanup_corrupted_files(self):
159
+ """Remove all checkpoint files if corruption is detected."""
160
+ for file in self.output_dir.glob(f"{self.sample_name}_gene_expression*.npz"):
161
+ try:
162
+ file.unlink()
163
+ logging.info(f"Deleted corrupted checkpoint: {file}")
164
+ except Exception as e:
165
+ logging.warning(f"Failed to delete {file}: {e}")
166
+
167
+ def _cleanup_old_checkpoints(self, current_checkpoint):
168
+ """Remove old checkpoints, keeping only the latest."""
169
+ for checkpoint in self.output_dir.glob(f"{self.sample_name}_gene_expression_checkpoint_*.npz"):
170
+ if checkpoint != current_checkpoint:
171
+ try:
172
+ checkpoint.unlink()
173
+ except Exception as e:
174
+ logging.warning(f"Failed to delete old checkpoint {checkpoint}: {e}")