canns 0.14.3__py3-none-any.whl → 0.15.1__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.
- canns/analyzer/data/asa/__init__.py +56 -21
- canns/analyzer/data/asa/coho.py +21 -0
- canns/analyzer/data/asa/cohomap.py +453 -0
- canns/analyzer/data/asa/cohomap_vectors.py +365 -0
- canns/analyzer/data/asa/cohospace.py +155 -1165
- canns/analyzer/data/asa/cohospace_phase_centers.py +119 -0
- canns/analyzer/data/asa/cohospace_scatter.py +1115 -0
- canns/analyzer/data/asa/embedding.py +5 -7
- canns/analyzer/data/asa/fr.py +1 -8
- canns/analyzer/data/asa/path.py +70 -0
- canns/analyzer/data/asa/plotting.py +5 -30
- canns/analyzer/data/asa/utils.py +160 -0
- canns/analyzer/data/cell_classification/__init__.py +10 -0
- canns/analyzer/data/cell_classification/core/__init__.py +4 -0
- canns/analyzer/data/cell_classification/core/btn.py +272 -0
- canns/analyzer/data/cell_classification/visualization/__init__.py +3 -0
- canns/analyzer/data/cell_classification/visualization/btn_plots.py +241 -0
- canns/analyzer/visualization/__init__.py +2 -0
- canns/analyzer/visualization/core/config.py +20 -0
- canns/analyzer/visualization/theta_sweep_plots.py +142 -0
- canns/pipeline/asa/runner.py +19 -19
- canns/pipeline/asa_gui/__init__.py +5 -3
- canns/pipeline/asa_gui/analysis_modes/pathcompare_mode.py +3 -1
- canns/pipeline/asa_gui/core/runner.py +23 -23
- canns/pipeline/asa_gui/views/pages/preprocess_page.py +7 -12
- {canns-0.14.3.dist-info → canns-0.15.1.dist-info}/METADATA +1 -1
- {canns-0.14.3.dist-info → canns-0.15.1.dist-info}/RECORD +30 -23
- canns/analyzer/data/asa/filters.py +0 -208
- {canns-0.14.3.dist-info → canns-0.15.1.dist-info}/WHEEL +0 -0
- {canns-0.14.3.dist-info → canns-0.15.1.dist-info}/entry_points.txt +0 -0
- {canns-0.14.3.dist-info → canns-0.15.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Stripe vectors and diagnostics for CohoMap."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import numpy as np
|
|
9
|
+
from scipy.ndimage import rotate
|
|
10
|
+
|
|
11
|
+
from ...visualization.core import PlotConfig, finalize_figure
|
|
12
|
+
from .cohomap import fit_cohomap_stripes
|
|
13
|
+
from .utils import _ensure_parent_dir, _ensure_plot_config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _rot_para(params1: np.ndarray, params2: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
17
|
+
"""Transform stripe fit parameters to canonical orientation.
|
|
18
|
+
|
|
19
|
+
This function adjusts the orientation angles of stripe fit parameters to
|
|
20
|
+
align them with a canonical coordinate system. Unlike `_rot_coord()` which
|
|
21
|
+
transforms actual coordinate data, this operates on the fit parameters
|
|
22
|
+
themselves (orientation angles, wavelengths, etc.).
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
params1 : np.ndarray
|
|
27
|
+
Stripe fit parameters for first dimension. First element is orientation angle.
|
|
28
|
+
params2 : np.ndarray
|
|
29
|
+
Stripe fit parameters for second dimension. First element is orientation angle.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
x : np.ndarray
|
|
34
|
+
Transformed parameters for the dimension with stronger horizontal alignment.
|
|
35
|
+
y : np.ndarray
|
|
36
|
+
Transformed parameters for the other dimension.
|
|
37
|
+
|
|
38
|
+
Notes
|
|
39
|
+
-----
|
|
40
|
+
The function selects which parameter set becomes 'x' and 'y' based on which
|
|
41
|
+
has larger |cos(angle)|, then applies angle adjustments to ensure the stripe
|
|
42
|
+
vectors are in a canonical orientation for visualization and analysis.
|
|
43
|
+
"""
|
|
44
|
+
if abs(np.cos(params1[0])) < abs(np.cos(params2[0])):
|
|
45
|
+
y = params1.copy()
|
|
46
|
+
x = params2.copy()
|
|
47
|
+
else:
|
|
48
|
+
x = params1.copy()
|
|
49
|
+
y = params2.copy()
|
|
50
|
+
|
|
51
|
+
alpha = y[0] - x[0]
|
|
52
|
+
if (alpha < 0) and (abs(alpha) > np.pi / 2):
|
|
53
|
+
x[0] += np.pi
|
|
54
|
+
x[0] = x[0] % (2 * np.pi)
|
|
55
|
+
elif (alpha < 0) and (abs(alpha) < np.pi / 2):
|
|
56
|
+
x[0] += np.pi * 4 / 3
|
|
57
|
+
x[0] = x[0] % (2 * np.pi)
|
|
58
|
+
elif abs(alpha) > np.pi / 2:
|
|
59
|
+
y[0] -= np.pi / 3
|
|
60
|
+
y[0] = y[0] % (2 * np.pi)
|
|
61
|
+
|
|
62
|
+
if y[0] > np.pi / 2:
|
|
63
|
+
y[0] -= np.pi / 6
|
|
64
|
+
x[0] -= np.pi / 6
|
|
65
|
+
x[0] = x[0] % (2 * np.pi)
|
|
66
|
+
y[0] = y[0] % (2 * np.pi)
|
|
67
|
+
if x[0] > np.pi / 2:
|
|
68
|
+
y[0] += np.pi / 6
|
|
69
|
+
x[0] += np.pi / 6
|
|
70
|
+
x[0] = x[0] % (2 * np.pi)
|
|
71
|
+
y[0] = y[0] % (2 * np.pi)
|
|
72
|
+
|
|
73
|
+
return x, y
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def cohomap_vectors(
|
|
77
|
+
cohomap_result: dict[str, Any],
|
|
78
|
+
*,
|
|
79
|
+
grid_size: int | None = 151,
|
|
80
|
+
trim: int = 25,
|
|
81
|
+
angle_grid: int = 10,
|
|
82
|
+
phase_grid: int = 10,
|
|
83
|
+
spacing_grid: int = 10,
|
|
84
|
+
spacing_range: tuple[float, float] = (1.0, 6.0),
|
|
85
|
+
) -> dict[str, Any]:
|
|
86
|
+
"""
|
|
87
|
+
Fit CohoMap stripe parameters and compute parallelogram vectors (v, w).
|
|
88
|
+
|
|
89
|
+
Returns a dict containing the stripe fit, rotated parameters, vector components,
|
|
90
|
+
and angle (deg) following GridCellTorus conventions.
|
|
91
|
+
"""
|
|
92
|
+
phase_map1 = np.asarray(cohomap_result["phase_map1"])
|
|
93
|
+
phase_map2 = np.asarray(cohomap_result["phase_map2"])
|
|
94
|
+
x_edge = np.asarray(cohomap_result["x_edge"])
|
|
95
|
+
y_edge = np.asarray(cohomap_result["y_edge"])
|
|
96
|
+
|
|
97
|
+
p1, f1 = fit_cohomap_stripes(
|
|
98
|
+
phase_map1,
|
|
99
|
+
grid_size=grid_size,
|
|
100
|
+
trim=trim,
|
|
101
|
+
angle_grid=angle_grid,
|
|
102
|
+
phase_grid=phase_grid,
|
|
103
|
+
spacing_grid=spacing_grid,
|
|
104
|
+
spacing_range=spacing_range,
|
|
105
|
+
)
|
|
106
|
+
p2, f2 = fit_cohomap_stripes(
|
|
107
|
+
phase_map2,
|
|
108
|
+
grid_size=grid_size,
|
|
109
|
+
trim=trim,
|
|
110
|
+
angle_grid=angle_grid,
|
|
111
|
+
phase_grid=phase_grid,
|
|
112
|
+
spacing_grid=spacing_grid,
|
|
113
|
+
spacing_range=spacing_range,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
x_params, y_params = _rot_para(p1, p2)
|
|
117
|
+
|
|
118
|
+
x_edge_shift = x_edge - float(x_edge.min())
|
|
119
|
+
y_edge_shift = y_edge - float(y_edge.min())
|
|
120
|
+
xmax = float(x_edge_shift.max())
|
|
121
|
+
ymax = float(y_edge_shift.max())
|
|
122
|
+
|
|
123
|
+
v = np.array(
|
|
124
|
+
[
|
|
125
|
+
(1 / x_params[2]) * np.cos(x_params[0]) * xmax,
|
|
126
|
+
(1 / x_params[2]) * np.sin(x_params[0]) * xmax,
|
|
127
|
+
],
|
|
128
|
+
dtype=float,
|
|
129
|
+
)
|
|
130
|
+
w = np.array(
|
|
131
|
+
[
|
|
132
|
+
(1 / y_params[2]) * np.cos(y_params[0]) * ymax,
|
|
133
|
+
(1 / y_params[2]) * np.sin(y_params[0]) * ymax,
|
|
134
|
+
],
|
|
135
|
+
dtype=float,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
angle_deg = float(((y_params[0] - x_params[0]) / (2 * np.pi) * 360.0) % 360.0)
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
"params1": p1,
|
|
142
|
+
"params2": p2,
|
|
143
|
+
"fit_error1": float(f1),
|
|
144
|
+
"fit_error2": float(f2),
|
|
145
|
+
"x_params": x_params,
|
|
146
|
+
"y_params": y_params,
|
|
147
|
+
"x_edge": x_edge,
|
|
148
|
+
"y_edge": y_edge,
|
|
149
|
+
"x_range": xmax,
|
|
150
|
+
"y_range": ymax,
|
|
151
|
+
"v": v,
|
|
152
|
+
"w": w,
|
|
153
|
+
"len_v": float(1 / x_params[2] * xmax),
|
|
154
|
+
"len_w": float(1 / y_params[2] * ymax),
|
|
155
|
+
"angle_deg": angle_deg,
|
|
156
|
+
"grid_size": grid_size,
|
|
157
|
+
"trim": trim,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def plot_cohomap_vectors(
|
|
162
|
+
cohomap_vectors_result: dict[str, Any],
|
|
163
|
+
*,
|
|
164
|
+
config: PlotConfig | None = None,
|
|
165
|
+
save_path: str | None = None,
|
|
166
|
+
show: bool = False,
|
|
167
|
+
figsize: tuple[int, int] = (5, 5),
|
|
168
|
+
color: str = "#f28e2b",
|
|
169
|
+
) -> plt.Figure:
|
|
170
|
+
"""
|
|
171
|
+
Plot v/w vectors and the parallelogram in spatial coordinates.
|
|
172
|
+
"""
|
|
173
|
+
config = _ensure_plot_config(
|
|
174
|
+
config,
|
|
175
|
+
PlotConfig.for_static_plot,
|
|
176
|
+
title="CohoMap Vectors",
|
|
177
|
+
xlabel="",
|
|
178
|
+
ylabel="",
|
|
179
|
+
figsize=figsize,
|
|
180
|
+
save_path=save_path,
|
|
181
|
+
show=show,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
vmax = float(cohomap_vectors_result["x_range"])
|
|
185
|
+
wmax = float(cohomap_vectors_result["y_range"])
|
|
186
|
+
v = np.asarray(cohomap_vectors_result["v"], dtype=float)
|
|
187
|
+
w = np.asarray(cohomap_vectors_result["w"], dtype=float)
|
|
188
|
+
|
|
189
|
+
fig, ax = plt.subplots(1, 1, figsize=config.figsize)
|
|
190
|
+
|
|
191
|
+
ax.plot([0, 0], [0, wmax], "--", color="0.6", lw=1)
|
|
192
|
+
ax.plot([vmax, vmax], [0, wmax], "--", color="0.6", lw=1)
|
|
193
|
+
ax.plot([0, vmax], [0, 0], "--", color="0.6", lw=1)
|
|
194
|
+
ax.plot([0, vmax], [wmax, wmax], "--", color="0.6", lw=1)
|
|
195
|
+
|
|
196
|
+
ax.plot([0, v[0]], [0, v[1]], color=color, lw=3)
|
|
197
|
+
ax.plot([0, w[0]], [0, w[1]], color=color, lw=3)
|
|
198
|
+
ax.plot([v[0], v[0] + w[0]], [v[1], v[1] + w[1]], color=color, lw=3)
|
|
199
|
+
ax.plot([w[0], v[0] + w[0]], [w[1], v[1] + w[1]], color=color, lw=3)
|
|
200
|
+
|
|
201
|
+
pad_x = 0.05 * vmax if vmax > 0 else 1.0
|
|
202
|
+
pad_y = 0.05 * wmax if wmax > 0 else 1.0
|
|
203
|
+
ax.set_xlim(-pad_x, vmax + pad_x)
|
|
204
|
+
ax.set_ylim(-pad_y, wmax + pad_y)
|
|
205
|
+
ax.set_aspect("equal", "box")
|
|
206
|
+
ax.set_xticks([])
|
|
207
|
+
ax.set_yticks([])
|
|
208
|
+
|
|
209
|
+
fig.tight_layout()
|
|
210
|
+
_ensure_parent_dir(config.save_path)
|
|
211
|
+
finalize_figure(fig, config)
|
|
212
|
+
return fig
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _resolve_grid_size(phase_map: np.ndarray, grid_size: int | None, trim: int) -> int:
|
|
216
|
+
"""Determine grid size for stripe fitting.
|
|
217
|
+
|
|
218
|
+
Parameters
|
|
219
|
+
----------
|
|
220
|
+
phase_map : np.ndarray
|
|
221
|
+
Phase map array (2D grid).
|
|
222
|
+
grid_size : int, optional
|
|
223
|
+
Explicit grid size. If None, inferred from phase_map shape.
|
|
224
|
+
trim : int
|
|
225
|
+
Number of edge bins to trim.
|
|
226
|
+
|
|
227
|
+
Returns
|
|
228
|
+
-------
|
|
229
|
+
int
|
|
230
|
+
Grid size to use for stripe fitting.
|
|
231
|
+
"""
|
|
232
|
+
if grid_size is None:
|
|
233
|
+
return int(phase_map.shape[0]) + 2 * trim + 1
|
|
234
|
+
return int(grid_size)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _stripe_fit_map(params: np.ndarray, grid_size: int, trim: int) -> np.ndarray:
|
|
238
|
+
"""Generate a synthetic stripe pattern from fit parameters.
|
|
239
|
+
|
|
240
|
+
Creates a 2D cosine stripe pattern based on the fitted parameters (orientation,
|
|
241
|
+
phase, and spatial frequency). Used for visualizing the fitted stripe model
|
|
242
|
+
and comparing it with the actual phase map.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
params : np.ndarray
|
|
247
|
+
Stripe fit parameters: [angle, phase, frequency].
|
|
248
|
+
- angle: Orientation angle in radians
|
|
249
|
+
- phase: Phase offset in radians
|
|
250
|
+
- frequency: Spatial frequency (inverse wavelength)
|
|
251
|
+
grid_size : int
|
|
252
|
+
Size of the grid to generate (before trimming).
|
|
253
|
+
trim : int
|
|
254
|
+
Number of edge bins to trim from the generated pattern.
|
|
255
|
+
|
|
256
|
+
Returns
|
|
257
|
+
-------
|
|
258
|
+
np.ndarray
|
|
259
|
+
2D array of shape (grid_size-2*trim, grid_size-2*trim) containing
|
|
260
|
+
the cosine stripe pattern with values in [-1, 1].
|
|
261
|
+
|
|
262
|
+
Notes
|
|
263
|
+
-----
|
|
264
|
+
The pattern is generated on a [0, 3π] × [0, 3π] domain to cover the
|
|
265
|
+
extended torus space, then rotated by the fitted angle and trimmed.
|
|
266
|
+
"""
|
|
267
|
+
numangsint = grid_size
|
|
268
|
+
x, _ = np.meshgrid(
|
|
269
|
+
np.linspace(0, 3 * np.pi, numangsint - 1),
|
|
270
|
+
np.linspace(0, 3 * np.pi, numangsint - 1),
|
|
271
|
+
)
|
|
272
|
+
x_rot = rotate(x, params[0] * 360.0 / (2 * np.pi), reshape=False)
|
|
273
|
+
return np.cos(params[2] * x_rot[trim:-trim, trim:-trim] + params[1])
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def plot_cohomap_stripes(
|
|
277
|
+
cohomap_result: dict[str, Any],
|
|
278
|
+
*,
|
|
279
|
+
cohomap_vectors_result: dict[str, Any] | None = None,
|
|
280
|
+
grid_size: int | None = 151,
|
|
281
|
+
trim: int = 25,
|
|
282
|
+
angle_grid: int = 10,
|
|
283
|
+
phase_grid: int = 10,
|
|
284
|
+
spacing_grid: int = 10,
|
|
285
|
+
spacing_range: tuple[float, float] = (1.0, 6.0),
|
|
286
|
+
config: PlotConfig | None = None,
|
|
287
|
+
save_path: str | None = None,
|
|
288
|
+
show: bool = False,
|
|
289
|
+
figsize: tuple[int, int] = (10, 6),
|
|
290
|
+
cmap: str = "viridis",
|
|
291
|
+
) -> plt.Figure:
|
|
292
|
+
"""
|
|
293
|
+
Plot stripe fit diagnostics for CohoMap (observed vs fitted stripes).
|
|
294
|
+
"""
|
|
295
|
+
if cohomap_vectors_result is None:
|
|
296
|
+
cohomap_vectors_result = cohomap_vectors(
|
|
297
|
+
cohomap_result,
|
|
298
|
+
grid_size=grid_size,
|
|
299
|
+
trim=trim,
|
|
300
|
+
angle_grid=angle_grid,
|
|
301
|
+
phase_grid=phase_grid,
|
|
302
|
+
spacing_grid=spacing_grid,
|
|
303
|
+
spacing_range=spacing_range,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if "grid_size" in cohomap_vectors_result:
|
|
307
|
+
grid_size = cohomap_vectors_result["grid_size"]
|
|
308
|
+
if "trim" in cohomap_vectors_result:
|
|
309
|
+
trim = cohomap_vectors_result["trim"]
|
|
310
|
+
|
|
311
|
+
config = _ensure_plot_config(
|
|
312
|
+
config,
|
|
313
|
+
PlotConfig.for_static_plot,
|
|
314
|
+
title="CohoMap Stripe Fit",
|
|
315
|
+
xlabel="",
|
|
316
|
+
ylabel="",
|
|
317
|
+
figsize=figsize,
|
|
318
|
+
save_path=save_path,
|
|
319
|
+
show=show,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
phase_map1 = np.asarray(cohomap_result["phase_map1"])
|
|
323
|
+
phase_map2 = np.asarray(cohomap_result["phase_map2"])
|
|
324
|
+
x_edge = np.asarray(cohomap_result["x_edge"])
|
|
325
|
+
y_edge = np.asarray(cohomap_result["y_edge"])
|
|
326
|
+
|
|
327
|
+
grid_size = _resolve_grid_size(phase_map1, grid_size, trim)
|
|
328
|
+
expected = grid_size - 1 - 2 * trim
|
|
329
|
+
if phase_map1.shape[0] != expected or phase_map2.shape[0] != expected:
|
|
330
|
+
raise ValueError("phase_map shape does not match grid_size/trim")
|
|
331
|
+
|
|
332
|
+
p1 = np.asarray(cohomap_vectors_result["params1"])
|
|
333
|
+
p2 = np.asarray(cohomap_vectors_result["params2"])
|
|
334
|
+
fit1 = _stripe_fit_map(p1, grid_size, trim)
|
|
335
|
+
fit2 = _stripe_fit_map(p2, grid_size, trim)
|
|
336
|
+
|
|
337
|
+
obs1 = np.cos(phase_map1)
|
|
338
|
+
obs2 = np.cos(phase_map2)
|
|
339
|
+
|
|
340
|
+
fig, axes = plt.subplots(2, 2, figsize=config.figsize)
|
|
341
|
+
panels = [
|
|
342
|
+
(obs1, "Phase Map 1 (cos)"),
|
|
343
|
+
(fit1, "Stripe Fit 1"),
|
|
344
|
+
(obs2, "Phase Map 2 (cos)"),
|
|
345
|
+
(fit2, "Stripe Fit 2"),
|
|
346
|
+
]
|
|
347
|
+
|
|
348
|
+
for ax, (img, title) in zip(axes.flat, panels, strict=True):
|
|
349
|
+
ax.imshow(
|
|
350
|
+
img,
|
|
351
|
+
origin="lower",
|
|
352
|
+
extent=[x_edge[0], x_edge[-1], y_edge[0], y_edge[-1]],
|
|
353
|
+
cmap=cmap,
|
|
354
|
+
vmin=-1.0,
|
|
355
|
+
vmax=1.0,
|
|
356
|
+
)
|
|
357
|
+
ax.set_title(title, fontsize=10)
|
|
358
|
+
ax.set_aspect("equal", "box")
|
|
359
|
+
ax.set_xticks([])
|
|
360
|
+
ax.set_yticks([])
|
|
361
|
+
|
|
362
|
+
fig.tight_layout()
|
|
363
|
+
_ensure_parent_dir(config.save_path)
|
|
364
|
+
finalize_figure(fig, config)
|
|
365
|
+
return fig
|