chatspatial 1.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.
- chatspatial/__init__.py +11 -0
- chatspatial/__main__.py +141 -0
- chatspatial/cli/__init__.py +7 -0
- chatspatial/config.py +53 -0
- chatspatial/models/__init__.py +85 -0
- chatspatial/models/analysis.py +513 -0
- chatspatial/models/data.py +2462 -0
- chatspatial/server.py +1763 -0
- chatspatial/spatial_mcp_adapter.py +720 -0
- chatspatial/tools/__init__.py +3 -0
- chatspatial/tools/annotation.py +1903 -0
- chatspatial/tools/cell_communication.py +1603 -0
- chatspatial/tools/cnv_analysis.py +605 -0
- chatspatial/tools/condition_comparison.py +595 -0
- chatspatial/tools/deconvolution/__init__.py +402 -0
- chatspatial/tools/deconvolution/base.py +318 -0
- chatspatial/tools/deconvolution/card.py +244 -0
- chatspatial/tools/deconvolution/cell2location.py +326 -0
- chatspatial/tools/deconvolution/destvi.py +144 -0
- chatspatial/tools/deconvolution/flashdeconv.py +101 -0
- chatspatial/tools/deconvolution/rctd.py +317 -0
- chatspatial/tools/deconvolution/spotlight.py +216 -0
- chatspatial/tools/deconvolution/stereoscope.py +109 -0
- chatspatial/tools/deconvolution/tangram.py +135 -0
- chatspatial/tools/differential.py +625 -0
- chatspatial/tools/embeddings.py +298 -0
- chatspatial/tools/enrichment.py +1863 -0
- chatspatial/tools/integration.py +807 -0
- chatspatial/tools/preprocessing.py +723 -0
- chatspatial/tools/spatial_domains.py +808 -0
- chatspatial/tools/spatial_genes.py +836 -0
- chatspatial/tools/spatial_registration.py +441 -0
- chatspatial/tools/spatial_statistics.py +1476 -0
- chatspatial/tools/trajectory.py +495 -0
- chatspatial/tools/velocity.py +405 -0
- chatspatial/tools/visualization/__init__.py +155 -0
- chatspatial/tools/visualization/basic.py +393 -0
- chatspatial/tools/visualization/cell_comm.py +699 -0
- chatspatial/tools/visualization/cnv.py +320 -0
- chatspatial/tools/visualization/core.py +684 -0
- chatspatial/tools/visualization/deconvolution.py +852 -0
- chatspatial/tools/visualization/enrichment.py +660 -0
- chatspatial/tools/visualization/integration.py +205 -0
- chatspatial/tools/visualization/main.py +164 -0
- chatspatial/tools/visualization/multi_gene.py +739 -0
- chatspatial/tools/visualization/persistence.py +335 -0
- chatspatial/tools/visualization/spatial_stats.py +469 -0
- chatspatial/tools/visualization/trajectory.py +639 -0
- chatspatial/tools/visualization/velocity.py +411 -0
- chatspatial/utils/__init__.py +115 -0
- chatspatial/utils/adata_utils.py +1372 -0
- chatspatial/utils/compute.py +327 -0
- chatspatial/utils/data_loader.py +499 -0
- chatspatial/utils/dependency_manager.py +462 -0
- chatspatial/utils/device_utils.py +165 -0
- chatspatial/utils/exceptions.py +185 -0
- chatspatial/utils/image_utils.py +267 -0
- chatspatial/utils/mcp_utils.py +137 -0
- chatspatial/utils/path_utils.py +243 -0
- chatspatial/utils/persistence.py +78 -0
- chatspatial/utils/scipy_compat.py +143 -0
- chatspatial-1.1.0.dist-info/METADATA +242 -0
- chatspatial-1.1.0.dist-info/RECORD +67 -0
- chatspatial-1.1.0.dist-info/WHEEL +5 -0
- chatspatial-1.1.0.dist-info/entry_points.txt +2 -0
- chatspatial-1.1.0.dist-info/licenses/LICENSE +21 -0
- chatspatial-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Multi-gene and ligand-receptor visualization functions.
|
|
3
|
+
|
|
4
|
+
This module contains:
|
|
5
|
+
- Multi-gene spatial visualization
|
|
6
|
+
- Multi-gene UMAP visualization
|
|
7
|
+
- Ligand-receptor pairs visualization
|
|
8
|
+
- Gene correlation visualization
|
|
9
|
+
- Spatial interaction visualization
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Optional
|
|
13
|
+
|
|
14
|
+
import matplotlib.pyplot as plt
|
|
15
|
+
import numpy as np
|
|
16
|
+
import scanpy as sc
|
|
17
|
+
from mpl_toolkits.axes_grid1 import make_axes_locatable
|
|
18
|
+
|
|
19
|
+
from ...models.data import VisualizationParameters
|
|
20
|
+
from ...utils.adata_utils import (
|
|
21
|
+
ensure_unique_var_names,
|
|
22
|
+
get_gene_expression,
|
|
23
|
+
get_genes_expression,
|
|
24
|
+
require_spatial_coords,
|
|
25
|
+
)
|
|
26
|
+
from ...utils.exceptions import DataNotFoundError, ParameterError, ProcessingError
|
|
27
|
+
from .core import (
|
|
28
|
+
create_figure,
|
|
29
|
+
get_validated_features,
|
|
30
|
+
plot_spatial_feature,
|
|
31
|
+
setup_multi_panel_figure,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
import anndata as ad
|
|
36
|
+
|
|
37
|
+
from ...spatial_mcp_adapter import ToolContext
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# =============================================================================
|
|
41
|
+
# Multi-Gene Visualization
|
|
42
|
+
# =============================================================================
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def create_multi_gene_visualization(
|
|
46
|
+
adata: "ad.AnnData",
|
|
47
|
+
params: VisualizationParameters,
|
|
48
|
+
context: Optional["ToolContext"] = None,
|
|
49
|
+
) -> plt.Figure:
|
|
50
|
+
"""Create multi-gene spatial visualization.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
adata: AnnData object
|
|
54
|
+
params: Visualization parameters
|
|
55
|
+
context: MCP context for logging
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
matplotlib Figure with multi-gene spatial visualization
|
|
59
|
+
"""
|
|
60
|
+
# Get validated features
|
|
61
|
+
available_genes = await get_validated_features(
|
|
62
|
+
adata, params, max_features=12, context=context
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if context:
|
|
66
|
+
await context.info(
|
|
67
|
+
f"Visualizing {len(available_genes)} genes: {available_genes}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Setup multi-panel figure
|
|
71
|
+
fig, axes = setup_multi_panel_figure(
|
|
72
|
+
n_panels=len(available_genes),
|
|
73
|
+
params=params,
|
|
74
|
+
default_title="",
|
|
75
|
+
use_tight_layout=False,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Use unique temporary column name to avoid conflicts
|
|
79
|
+
temp_feature_key = "multi_gene_expr_temp_viz_99_unique"
|
|
80
|
+
|
|
81
|
+
for i, gene in enumerate(available_genes):
|
|
82
|
+
if i < len(axes):
|
|
83
|
+
ax = axes[i]
|
|
84
|
+
try:
|
|
85
|
+
# Get gene expression using unified utility
|
|
86
|
+
gene_expr = get_gene_expression(adata, gene)
|
|
87
|
+
|
|
88
|
+
# Apply color scaling
|
|
89
|
+
if params.color_scale == "log":
|
|
90
|
+
gene_expr = np.log1p(gene_expr)
|
|
91
|
+
elif params.color_scale == "sqrt":
|
|
92
|
+
gene_expr = np.sqrt(gene_expr)
|
|
93
|
+
|
|
94
|
+
# Add temporary column
|
|
95
|
+
adata.obs[temp_feature_key] = gene_expr
|
|
96
|
+
|
|
97
|
+
# Create spatial plot
|
|
98
|
+
if "spatial" in adata.obsm:
|
|
99
|
+
plot_spatial_feature(
|
|
100
|
+
adata,
|
|
101
|
+
ax=ax,
|
|
102
|
+
feature=temp_feature_key,
|
|
103
|
+
params=params,
|
|
104
|
+
show_colorbar=False,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Set color limits
|
|
108
|
+
vmin = (
|
|
109
|
+
params.vmin
|
|
110
|
+
if params.vmin is not None
|
|
111
|
+
else np.percentile(gene_expr, 1)
|
|
112
|
+
)
|
|
113
|
+
vmax = (
|
|
114
|
+
params.vmax
|
|
115
|
+
if params.vmax is not None
|
|
116
|
+
else np.percentile(gene_expr, 99)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Update colorbar limits
|
|
120
|
+
scatter = ax.collections[0] if ax.collections else None
|
|
121
|
+
if scatter:
|
|
122
|
+
scatter.set_clim(vmin, vmax)
|
|
123
|
+
|
|
124
|
+
# Add colorbar
|
|
125
|
+
if params.show_colorbar:
|
|
126
|
+
divider = make_axes_locatable(ax)
|
|
127
|
+
cax = divider.append_axes(
|
|
128
|
+
"right",
|
|
129
|
+
size=params.colorbar_size,
|
|
130
|
+
pad=params.colorbar_pad,
|
|
131
|
+
)
|
|
132
|
+
plt.colorbar(scatter, cax=cax)
|
|
133
|
+
|
|
134
|
+
ax.invert_yaxis()
|
|
135
|
+
if params.add_gene_labels:
|
|
136
|
+
ax.set_title(gene, fontsize=12)
|
|
137
|
+
else:
|
|
138
|
+
# Fallback: histogram
|
|
139
|
+
ax.hist(gene_expr, bins=30, alpha=params.alpha, color="steelblue")
|
|
140
|
+
ax.set_xlabel("Expression")
|
|
141
|
+
ax.set_ylabel("Frequency")
|
|
142
|
+
if params.add_gene_labels:
|
|
143
|
+
ax.set_title(gene, fontsize=12)
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
ax.text(
|
|
147
|
+
0.5,
|
|
148
|
+
0.5,
|
|
149
|
+
f"Error plotting {gene}:\n{e}",
|
|
150
|
+
ha="center",
|
|
151
|
+
va="center",
|
|
152
|
+
transform=ax.transAxes,
|
|
153
|
+
)
|
|
154
|
+
if params.add_gene_labels:
|
|
155
|
+
ax.set_title(f"{gene} (Error)", fontsize=12)
|
|
156
|
+
|
|
157
|
+
# Clean up temporary column
|
|
158
|
+
if temp_feature_key in adata.obs:
|
|
159
|
+
del adata.obs[temp_feature_key]
|
|
160
|
+
|
|
161
|
+
# Adjust spacing
|
|
162
|
+
fig.subplots_adjust(top=0.92, wspace=0.1, hspace=0.3, right=0.98)
|
|
163
|
+
return fig
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
async def create_multi_gene_umap_visualization(
|
|
167
|
+
adata: "ad.AnnData",
|
|
168
|
+
params: VisualizationParameters,
|
|
169
|
+
context: Optional["ToolContext"] = None,
|
|
170
|
+
) -> plt.Figure:
|
|
171
|
+
"""Create multi-gene UMAP visualization.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
adata: AnnData object
|
|
175
|
+
params: Visualization parameters
|
|
176
|
+
context: MCP context for logging
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
matplotlib Figure with multi-gene UMAP visualization
|
|
180
|
+
"""
|
|
181
|
+
# Get validated features
|
|
182
|
+
available_genes = await get_validated_features(
|
|
183
|
+
adata, params, max_features=12, context=context
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if context:
|
|
187
|
+
await context.info(
|
|
188
|
+
f"Creating multi-panel UMAP plot for features: {available_genes}"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Setup multi-panel figure
|
|
192
|
+
fig, axes = setup_multi_panel_figure(
|
|
193
|
+
n_panels=len(available_genes),
|
|
194
|
+
params=params,
|
|
195
|
+
default_title="",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Use unique temporary column name
|
|
199
|
+
temp_feature_key = "umap_gene_temp_viz_99_unique"
|
|
200
|
+
|
|
201
|
+
for i, gene in enumerate(available_genes):
|
|
202
|
+
if i < len(axes):
|
|
203
|
+
ax = axes[i]
|
|
204
|
+
try:
|
|
205
|
+
# Get gene expression using unified utility
|
|
206
|
+
gene_expr = get_gene_expression(adata, gene)
|
|
207
|
+
|
|
208
|
+
# Apply color scaling
|
|
209
|
+
if params.color_scale == "log":
|
|
210
|
+
gene_expr = np.log1p(gene_expr)
|
|
211
|
+
elif params.color_scale == "sqrt":
|
|
212
|
+
gene_expr = np.sqrt(gene_expr)
|
|
213
|
+
|
|
214
|
+
# Add temporary column
|
|
215
|
+
adata.obs[temp_feature_key] = gene_expr
|
|
216
|
+
|
|
217
|
+
# Set color scale
|
|
218
|
+
vmin = 0
|
|
219
|
+
vmax = max(gene_expr.max(), 0.1)
|
|
220
|
+
|
|
221
|
+
# Use percentile-based scaling for sparse data
|
|
222
|
+
if np.sum(gene_expr > 0) > 10:
|
|
223
|
+
vmax = np.percentile(gene_expr[gene_expr > 0], 95)
|
|
224
|
+
|
|
225
|
+
sc.pl.umap(
|
|
226
|
+
adata,
|
|
227
|
+
color=temp_feature_key,
|
|
228
|
+
cmap=params.colormap,
|
|
229
|
+
ax=ax,
|
|
230
|
+
show=False,
|
|
231
|
+
frameon=False,
|
|
232
|
+
vmin=vmin,
|
|
233
|
+
vmax=vmax,
|
|
234
|
+
colorbar_loc="right",
|
|
235
|
+
)
|
|
236
|
+
ax.set_title(gene)
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
ax.text(
|
|
240
|
+
0.5,
|
|
241
|
+
0.5,
|
|
242
|
+
f"Error plotting {gene}:\n{e}",
|
|
243
|
+
ha="center",
|
|
244
|
+
va="center",
|
|
245
|
+
transform=ax.transAxes,
|
|
246
|
+
)
|
|
247
|
+
ax.set_title(f"{gene} (Error)")
|
|
248
|
+
|
|
249
|
+
# Clean up temporary column
|
|
250
|
+
if temp_feature_key in adata.obs:
|
|
251
|
+
del adata.obs[temp_feature_key]
|
|
252
|
+
|
|
253
|
+
return fig
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# =============================================================================
|
|
257
|
+
# Ligand-Receptor Pairs Visualization
|
|
258
|
+
# =============================================================================
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _parse_lr_pairs(
|
|
262
|
+
adata: "ad.AnnData",
|
|
263
|
+
params: VisualizationParameters,
|
|
264
|
+
) -> list[tuple[str, str]]:
|
|
265
|
+
"""Parse ligand-receptor pairs from various sources.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
adata: AnnData object
|
|
269
|
+
params: Visualization parameters
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
List of (ligand, receptor) tuples
|
|
273
|
+
|
|
274
|
+
Raises:
|
|
275
|
+
DataNotFoundError: If no LR pairs found
|
|
276
|
+
"""
|
|
277
|
+
lr_pairs = []
|
|
278
|
+
|
|
279
|
+
# 1. Check for explicit lr_pairs parameter
|
|
280
|
+
if params.lr_pairs:
|
|
281
|
+
lr_pairs = params.lr_pairs
|
|
282
|
+
else:
|
|
283
|
+
# 2. Try to parse from feature parameter
|
|
284
|
+
feature_list = (
|
|
285
|
+
params.feature
|
|
286
|
+
if isinstance(params.feature, list)
|
|
287
|
+
else ([params.feature] if params.feature else [])
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
if feature_list:
|
|
291
|
+
has_special_format = any(
|
|
292
|
+
"^" in str(f) or ("_" in str(f) and not str(f).startswith("_"))
|
|
293
|
+
for f in feature_list
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
if has_special_format:
|
|
297
|
+
for item in feature_list:
|
|
298
|
+
# Handle "Ligand^Receptor" format from LIANA
|
|
299
|
+
if "^" in str(item):
|
|
300
|
+
ligand, receptor = str(item).split("^", 1)
|
|
301
|
+
lr_pairs.append((ligand, receptor))
|
|
302
|
+
# Handle "Ligand_Receptor" format
|
|
303
|
+
elif "_" in str(item) and not str(item).startswith("_"):
|
|
304
|
+
parts = str(item).split("_")
|
|
305
|
+
if len(parts) == 2:
|
|
306
|
+
lr_pairs.append((parts[0], parts[1]))
|
|
307
|
+
|
|
308
|
+
# 3. Try to get from stored analysis results
|
|
309
|
+
if not lr_pairs and hasattr(adata, "uns"):
|
|
310
|
+
if "detected_lr_pairs" in adata.uns:
|
|
311
|
+
lr_pairs = adata.uns["detected_lr_pairs"]
|
|
312
|
+
elif "cell_communication_results" in adata.uns:
|
|
313
|
+
comm_results = adata.uns["cell_communication_results"]
|
|
314
|
+
if "top_lr_pairs" in comm_results:
|
|
315
|
+
for pair_str in comm_results["top_lr_pairs"]:
|
|
316
|
+
if "^" in pair_str:
|
|
317
|
+
ligand, receptor = pair_str.split("^", 1)
|
|
318
|
+
lr_pairs.append((ligand, receptor))
|
|
319
|
+
elif "_" in pair_str:
|
|
320
|
+
parts = pair_str.split("_")
|
|
321
|
+
if len(parts) == 2:
|
|
322
|
+
lr_pairs.append((parts[0], parts[1]))
|
|
323
|
+
|
|
324
|
+
# No hardcoded defaults - scientific integrity
|
|
325
|
+
if not lr_pairs:
|
|
326
|
+
raise DataNotFoundError(
|
|
327
|
+
"No ligand-receptor pairs to visualize.\n\n"
|
|
328
|
+
"Options:\n"
|
|
329
|
+
"1. Run cell communication analysis first\n"
|
|
330
|
+
"2. Specify lr_pairs parameter: lr_pairs=[('Ligand', 'Receptor')]\n"
|
|
331
|
+
"3. Use LIANA format: feature=['Ligand^Receptor']\n"
|
|
332
|
+
"4. Use underscore format: feature=['Ligand_Receptor']"
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
return lr_pairs
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
async def create_lr_pairs_visualization(
|
|
339
|
+
adata: "ad.AnnData",
|
|
340
|
+
params: VisualizationParameters,
|
|
341
|
+
context: Optional["ToolContext"] = None,
|
|
342
|
+
) -> plt.Figure:
|
|
343
|
+
"""Create ligand-receptor pairs visualization.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
adata: AnnData object
|
|
347
|
+
params: Visualization parameters
|
|
348
|
+
context: MCP context for logging
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
matplotlib Figure with LR pairs visualization
|
|
352
|
+
"""
|
|
353
|
+
from scipy.stats import kendalltau, pearsonr, spearmanr
|
|
354
|
+
|
|
355
|
+
# Ensure unique gene names
|
|
356
|
+
ensure_unique_var_names(adata)
|
|
357
|
+
|
|
358
|
+
# Parse LR pairs
|
|
359
|
+
lr_pairs = _parse_lr_pairs(adata, params)
|
|
360
|
+
|
|
361
|
+
# Filter pairs where both genes exist
|
|
362
|
+
available_pairs = []
|
|
363
|
+
for ligand, receptor in lr_pairs:
|
|
364
|
+
if ligand in adata.var_names and receptor in adata.var_names:
|
|
365
|
+
available_pairs.append((ligand, receptor))
|
|
366
|
+
|
|
367
|
+
if not available_pairs:
|
|
368
|
+
raise DataNotFoundError(
|
|
369
|
+
f"None of the specified LR pairs found in data: {lr_pairs}"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Limit to avoid overly large plots
|
|
373
|
+
max_pairs = 4
|
|
374
|
+
if len(available_pairs) > max_pairs:
|
|
375
|
+
if context:
|
|
376
|
+
await context.warning(
|
|
377
|
+
f"Too many LR pairs ({len(available_pairs)}). Limiting to first {max_pairs}."
|
|
378
|
+
)
|
|
379
|
+
available_pairs = available_pairs[:max_pairs]
|
|
380
|
+
|
|
381
|
+
if context:
|
|
382
|
+
await context.info(
|
|
383
|
+
f"Visualizing {len(available_pairs)} LR pairs: {available_pairs}"
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# Each pair gets 3 panels: ligand, receptor, correlation
|
|
387
|
+
n_panels = len(available_pairs) * 3
|
|
388
|
+
|
|
389
|
+
fig, axes = setup_multi_panel_figure(
|
|
390
|
+
n_panels=n_panels,
|
|
391
|
+
params=params,
|
|
392
|
+
default_title=f"Ligand-Receptor Pairs ({len(available_pairs)} pairs)",
|
|
393
|
+
use_tight_layout=True,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Use unique temporary column name
|
|
397
|
+
temp_feature_key = "lr_expr_temp_viz_99_unique"
|
|
398
|
+
ax_idx = 0
|
|
399
|
+
|
|
400
|
+
for _pair_idx, (ligand, receptor) in enumerate(available_pairs):
|
|
401
|
+
try:
|
|
402
|
+
# Get expression data using unified utility
|
|
403
|
+
ligand_expr = get_gene_expression(adata, ligand)
|
|
404
|
+
receptor_expr = get_gene_expression(adata, receptor)
|
|
405
|
+
|
|
406
|
+
# Apply color scaling
|
|
407
|
+
if params.color_scale == "log":
|
|
408
|
+
ligand_expr = np.log1p(ligand_expr)
|
|
409
|
+
receptor_expr = np.log1p(receptor_expr)
|
|
410
|
+
elif params.color_scale == "sqrt":
|
|
411
|
+
ligand_expr = np.sqrt(ligand_expr)
|
|
412
|
+
receptor_expr = np.sqrt(receptor_expr)
|
|
413
|
+
|
|
414
|
+
# Plot ligand
|
|
415
|
+
if ax_idx < len(axes) and "spatial" in adata.obsm:
|
|
416
|
+
ax = axes[ax_idx]
|
|
417
|
+
adata.obs[temp_feature_key] = ligand_expr
|
|
418
|
+
plot_spatial_feature(
|
|
419
|
+
adata,
|
|
420
|
+
ax=ax,
|
|
421
|
+
feature=temp_feature_key,
|
|
422
|
+
params=params,
|
|
423
|
+
show_colorbar=False,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
if params.show_colorbar and ax.collections:
|
|
427
|
+
divider = make_axes_locatable(ax)
|
|
428
|
+
cax = divider.append_axes(
|
|
429
|
+
"right",
|
|
430
|
+
size=params.colorbar_size,
|
|
431
|
+
pad=params.colorbar_pad,
|
|
432
|
+
)
|
|
433
|
+
plt.colorbar(ax.collections[-1], cax=cax)
|
|
434
|
+
|
|
435
|
+
ax.invert_yaxis()
|
|
436
|
+
if params.add_gene_labels:
|
|
437
|
+
ax.set_title(f"{ligand} (Ligand)", fontsize=10)
|
|
438
|
+
ax_idx += 1
|
|
439
|
+
|
|
440
|
+
# Plot receptor
|
|
441
|
+
if ax_idx < len(axes) and "spatial" in adata.obsm:
|
|
442
|
+
ax = axes[ax_idx]
|
|
443
|
+
adata.obs[temp_feature_key] = receptor_expr
|
|
444
|
+
plot_spatial_feature(
|
|
445
|
+
adata,
|
|
446
|
+
ax=ax,
|
|
447
|
+
feature=temp_feature_key,
|
|
448
|
+
params=params,
|
|
449
|
+
show_colorbar=False,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
if params.show_colorbar and ax.collections:
|
|
453
|
+
divider = make_axes_locatable(ax)
|
|
454
|
+
cax = divider.append_axes(
|
|
455
|
+
"right",
|
|
456
|
+
size=params.colorbar_size,
|
|
457
|
+
pad=params.colorbar_pad,
|
|
458
|
+
)
|
|
459
|
+
plt.colorbar(ax.collections[-1], cax=cax)
|
|
460
|
+
|
|
461
|
+
ax.invert_yaxis()
|
|
462
|
+
if params.add_gene_labels:
|
|
463
|
+
ax.set_title(f"{receptor} (Receptor)", fontsize=10)
|
|
464
|
+
ax_idx += 1
|
|
465
|
+
|
|
466
|
+
# Plot correlation
|
|
467
|
+
if ax_idx < len(axes):
|
|
468
|
+
ax = axes[ax_idx]
|
|
469
|
+
|
|
470
|
+
# Calculate correlation
|
|
471
|
+
if params.correlation_method == "pearson":
|
|
472
|
+
corr, p_value = pearsonr(ligand_expr, receptor_expr)
|
|
473
|
+
elif params.correlation_method == "spearman":
|
|
474
|
+
corr, p_value = spearmanr(ligand_expr, receptor_expr)
|
|
475
|
+
else: # kendall
|
|
476
|
+
corr, p_value = kendalltau(ligand_expr, receptor_expr)
|
|
477
|
+
|
|
478
|
+
# Create scatter plot
|
|
479
|
+
ax.scatter(ligand_expr, receptor_expr, alpha=params.alpha, s=20)
|
|
480
|
+
ax.set_xlabel(f"{ligand} Expression")
|
|
481
|
+
ax.set_ylabel(f"{receptor} Expression")
|
|
482
|
+
|
|
483
|
+
if params.show_correlation_stats:
|
|
484
|
+
ax.set_title(
|
|
485
|
+
f"Correlation: {corr:.3f}\np-value: {p_value:.2e}",
|
|
486
|
+
fontsize=10,
|
|
487
|
+
)
|
|
488
|
+
else:
|
|
489
|
+
ax.set_title(f"{ligand} vs {receptor}", fontsize=10)
|
|
490
|
+
|
|
491
|
+
# Add trend line
|
|
492
|
+
z = np.polyfit(ligand_expr, receptor_expr, 1)
|
|
493
|
+
p = np.poly1d(z)
|
|
494
|
+
ax.plot(ligand_expr, p(ligand_expr), "r--", alpha=0.8)
|
|
495
|
+
|
|
496
|
+
ax_idx += 1
|
|
497
|
+
|
|
498
|
+
except Exception as e:
|
|
499
|
+
if ax_idx < len(axes):
|
|
500
|
+
ax = axes[ax_idx]
|
|
501
|
+
ax.text(
|
|
502
|
+
0.5,
|
|
503
|
+
0.5,
|
|
504
|
+
f"Error plotting {ligand}-{receptor}:\n{e}",
|
|
505
|
+
ha="center",
|
|
506
|
+
va="center",
|
|
507
|
+
transform=ax.transAxes,
|
|
508
|
+
)
|
|
509
|
+
ax.set_title(f"{ligand}-{receptor} (Error)", fontsize=10)
|
|
510
|
+
ax_idx += 1
|
|
511
|
+
|
|
512
|
+
# Clean up temporary column
|
|
513
|
+
if temp_feature_key in adata.obs:
|
|
514
|
+
del adata.obs[temp_feature_key]
|
|
515
|
+
|
|
516
|
+
# Adjust spacing
|
|
517
|
+
fig.subplots_adjust(top=0.92, wspace=0.1, hspace=0.3, right=0.98)
|
|
518
|
+
return fig
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
# =============================================================================
|
|
522
|
+
# Gene Correlation Visualization
|
|
523
|
+
# =============================================================================
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
async def create_gene_correlation_visualization(
|
|
527
|
+
adata: "ad.AnnData",
|
|
528
|
+
params: VisualizationParameters,
|
|
529
|
+
context: Optional["ToolContext"] = None,
|
|
530
|
+
) -> plt.Figure:
|
|
531
|
+
"""Create gene correlation visualization using seaborn clustermap.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
adata: AnnData object
|
|
535
|
+
params: Visualization parameters
|
|
536
|
+
context: MCP context for logging
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
matplotlib Figure with gene correlation clustermap
|
|
540
|
+
"""
|
|
541
|
+
import pandas as pd
|
|
542
|
+
import seaborn as sns
|
|
543
|
+
|
|
544
|
+
# Get validated genes
|
|
545
|
+
available_genes = await get_validated_features(
|
|
546
|
+
adata, params, max_features=10, context=context
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
if context:
|
|
550
|
+
await context.info(
|
|
551
|
+
f"Creating gene correlation visualization for {len(available_genes)} genes"
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
# Get expression matrix using unified utility
|
|
555
|
+
expr_matrix = get_genes_expression(adata, available_genes)
|
|
556
|
+
|
|
557
|
+
# Apply color scaling
|
|
558
|
+
if params.color_scale == "log":
|
|
559
|
+
expr_matrix = np.log1p(expr_matrix)
|
|
560
|
+
elif params.color_scale == "sqrt":
|
|
561
|
+
expr_matrix = np.sqrt(expr_matrix)
|
|
562
|
+
|
|
563
|
+
# Create DataFrame for correlation
|
|
564
|
+
expr_df = pd.DataFrame(expr_matrix, columns=available_genes)
|
|
565
|
+
|
|
566
|
+
# Calculate correlation
|
|
567
|
+
corr_df = expr_df.corr(method=params.correlation_method)
|
|
568
|
+
|
|
569
|
+
# Use seaborn clustermap
|
|
570
|
+
figsize = params.figure_size or (
|
|
571
|
+
max(8, len(available_genes)),
|
|
572
|
+
max(8, len(available_genes)),
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
g = sns.clustermap(
|
|
576
|
+
corr_df,
|
|
577
|
+
cmap=params.colormap,
|
|
578
|
+
center=0,
|
|
579
|
+
annot=True,
|
|
580
|
+
fmt=".2f",
|
|
581
|
+
square=True,
|
|
582
|
+
figsize=figsize,
|
|
583
|
+
dendrogram_ratio=0.15,
|
|
584
|
+
cbar_pos=(0.02, 0.8, 0.03, 0.15),
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
title = params.title or f"Gene Correlation ({params.correlation_method.title()})"
|
|
588
|
+
g.fig.suptitle(title, y=1.02, fontsize=14)
|
|
589
|
+
|
|
590
|
+
return g.fig
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
# =============================================================================
|
|
594
|
+
# Spatial Interaction Visualization
|
|
595
|
+
# =============================================================================
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
async def create_spatial_interaction_visualization(
|
|
599
|
+
adata: "ad.AnnData",
|
|
600
|
+
params: VisualizationParameters,
|
|
601
|
+
context: Optional["ToolContext"] = None,
|
|
602
|
+
) -> plt.Figure:
|
|
603
|
+
"""Create spatial visualization showing ligand-receptor interactions.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
adata: AnnData object
|
|
607
|
+
params: Visualization parameters
|
|
608
|
+
context: MCP context for logging
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
matplotlib Figure with spatial interaction visualization
|
|
612
|
+
"""
|
|
613
|
+
from scipy.spatial.distance import cdist
|
|
614
|
+
|
|
615
|
+
if context:
|
|
616
|
+
await context.info("Creating spatial interaction plot")
|
|
617
|
+
|
|
618
|
+
try:
|
|
619
|
+
# Get spatial coordinates
|
|
620
|
+
spatial_coords = require_spatial_coords(adata)
|
|
621
|
+
|
|
622
|
+
# Validate lr_pairs
|
|
623
|
+
if not params.lr_pairs or len(params.lr_pairs) == 0:
|
|
624
|
+
raise ParameterError(
|
|
625
|
+
"No ligand-receptor pairs provided for spatial interaction visualization"
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
# Create figure
|
|
629
|
+
fig, ax = create_figure(figsize=(12, 10))
|
|
630
|
+
|
|
631
|
+
# Plot all cells as background
|
|
632
|
+
ax.scatter(
|
|
633
|
+
spatial_coords[:, 0],
|
|
634
|
+
spatial_coords[:, 1],
|
|
635
|
+
c="lightgray",
|
|
636
|
+
s=10,
|
|
637
|
+
alpha=0.5,
|
|
638
|
+
label="All cells",
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
# Color mapping for different LR pairs
|
|
642
|
+
colors = plt.get_cmap("Set3")(np.linspace(0, 1, len(params.lr_pairs)))
|
|
643
|
+
|
|
644
|
+
interaction_count = 0
|
|
645
|
+
for i, (ligand, receptor) in enumerate(params.lr_pairs):
|
|
646
|
+
color = colors[i]
|
|
647
|
+
|
|
648
|
+
# Check if genes exist
|
|
649
|
+
if ligand in adata.var_names and receptor in adata.var_names:
|
|
650
|
+
# Get expression using unified utility
|
|
651
|
+
ligand_expr = get_gene_expression(adata, ligand)
|
|
652
|
+
receptor_expr = get_gene_expression(adata, receptor)
|
|
653
|
+
|
|
654
|
+
# Define expression threshold
|
|
655
|
+
ligand_threshold = (
|
|
656
|
+
np.median(ligand_expr[ligand_expr > 0])
|
|
657
|
+
if np.any(ligand_expr > 0)
|
|
658
|
+
else 0
|
|
659
|
+
)
|
|
660
|
+
receptor_threshold = (
|
|
661
|
+
np.median(receptor_expr[receptor_expr > 0])
|
|
662
|
+
if np.any(receptor_expr > 0)
|
|
663
|
+
else 0
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
# Find expressing cells
|
|
667
|
+
ligand_cells = ligand_expr > ligand_threshold
|
|
668
|
+
receptor_cells = receptor_expr > receptor_threshold
|
|
669
|
+
|
|
670
|
+
if np.any(ligand_cells) and np.any(receptor_cells):
|
|
671
|
+
# Plot ligand-expressing cells
|
|
672
|
+
ligand_coords = spatial_coords[ligand_cells]
|
|
673
|
+
ax.scatter(
|
|
674
|
+
ligand_coords[:, 0],
|
|
675
|
+
ligand_coords[:, 1],
|
|
676
|
+
c=[color],
|
|
677
|
+
s=50,
|
|
678
|
+
alpha=0.7,
|
|
679
|
+
marker="o",
|
|
680
|
+
label=f"{ligand}+ (Ligand)",
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
# Plot receptor-expressing cells
|
|
684
|
+
receptor_coords = spatial_coords[receptor_cells]
|
|
685
|
+
ax.scatter(
|
|
686
|
+
receptor_coords[:, 0],
|
|
687
|
+
receptor_coords[:, 1],
|
|
688
|
+
c=[color],
|
|
689
|
+
s=50,
|
|
690
|
+
alpha=0.7,
|
|
691
|
+
marker="^",
|
|
692
|
+
label=f"{receptor}+ (Receptor)",
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
# Draw connections
|
|
696
|
+
if len(ligand_coords) > 0 and len(receptor_coords) > 0:
|
|
697
|
+
distances = cdist(ligand_coords, receptor_coords)
|
|
698
|
+
distance_threshold = np.percentile(distances, 10)
|
|
699
|
+
|
|
700
|
+
ligand_indices, receptor_indices = np.where(
|
|
701
|
+
distances <= distance_threshold
|
|
702
|
+
)
|
|
703
|
+
for li, ri in zip(
|
|
704
|
+
ligand_indices[:50], receptor_indices[:50], strict=False
|
|
705
|
+
):
|
|
706
|
+
ax.plot(
|
|
707
|
+
[ligand_coords[li, 0], receptor_coords[ri, 0]],
|
|
708
|
+
[ligand_coords[li, 1], receptor_coords[ri, 1]],
|
|
709
|
+
color=color,
|
|
710
|
+
alpha=0.3,
|
|
711
|
+
linewidth=0.5,
|
|
712
|
+
)
|
|
713
|
+
interaction_count += 1
|
|
714
|
+
|
|
715
|
+
elif context:
|
|
716
|
+
await context.warning(
|
|
717
|
+
f"Genes {ligand} or {receptor} not found in expression data"
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
ax.set_xlabel("Spatial X")
|
|
721
|
+
ax.set_ylabel("Spatial Y")
|
|
722
|
+
ax.set_title(
|
|
723
|
+
f"Spatial Ligand-Receptor Interactions\n({interaction_count} connections shown)",
|
|
724
|
+
fontsize=14,
|
|
725
|
+
fontweight="bold",
|
|
726
|
+
)
|
|
727
|
+
ax.set_aspect("equal")
|
|
728
|
+
ax.legend(bbox_to_anchor=(1.05, 1), loc="upper left")
|
|
729
|
+
|
|
730
|
+
plt.tight_layout()
|
|
731
|
+
return fig
|
|
732
|
+
|
|
733
|
+
except ValueError:
|
|
734
|
+
raise
|
|
735
|
+
except Exception as e:
|
|
736
|
+
raise ProcessingError(
|
|
737
|
+
f"Spatial ligand-receptor interaction visualization failed: {e}\n\n"
|
|
738
|
+
f"Check gene names exist and spatial coordinates are available."
|
|
739
|
+
) from e
|