citegeist 0.1.0__py3-none-any.whl
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.
- citegeist/__init__.py +0 -0
- citegeist/core/__init__.py +21 -0
- citegeist/core/analysis_functions.py +42 -0
- citegeist/core/checkpoints.py +174 -0
- citegeist/core/citegeist_model.py +807 -0
- citegeist/core/gurobi_impl.py +1219 -0
- citegeist/core/utils.py +378 -0
- citegeist/py.typed +0 -0
- citegeist-0.1.0.dist-info/LICENSE +28 -0
- citegeist-0.1.0.dist-info/METADATA +199 -0
- citegeist-0.1.0.dist-info/RECORD +13 -0
- citegeist-0.1.0.dist-info/WHEEL +5 -0
- citegeist-0.1.0.dist-info/top_level.txt +1 -0
citegeist/__init__.py
ADDED
|
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}")
|