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.
Files changed (31) hide show
  1. canns/analyzer/data/asa/__init__.py +56 -21
  2. canns/analyzer/data/asa/coho.py +21 -0
  3. canns/analyzer/data/asa/cohomap.py +453 -0
  4. canns/analyzer/data/asa/cohomap_vectors.py +365 -0
  5. canns/analyzer/data/asa/cohospace.py +155 -1165
  6. canns/analyzer/data/asa/cohospace_phase_centers.py +119 -0
  7. canns/analyzer/data/asa/cohospace_scatter.py +1115 -0
  8. canns/analyzer/data/asa/embedding.py +5 -7
  9. canns/analyzer/data/asa/fr.py +1 -8
  10. canns/analyzer/data/asa/path.py +70 -0
  11. canns/analyzer/data/asa/plotting.py +5 -30
  12. canns/analyzer/data/asa/utils.py +160 -0
  13. canns/analyzer/data/cell_classification/__init__.py +10 -0
  14. canns/analyzer/data/cell_classification/core/__init__.py +4 -0
  15. canns/analyzer/data/cell_classification/core/btn.py +272 -0
  16. canns/analyzer/data/cell_classification/visualization/__init__.py +3 -0
  17. canns/analyzer/data/cell_classification/visualization/btn_plots.py +241 -0
  18. canns/analyzer/visualization/__init__.py +2 -0
  19. canns/analyzer/visualization/core/config.py +20 -0
  20. canns/analyzer/visualization/theta_sweep_plots.py +142 -0
  21. canns/pipeline/asa/runner.py +19 -19
  22. canns/pipeline/asa_gui/__init__.py +5 -3
  23. canns/pipeline/asa_gui/analysis_modes/pathcompare_mode.py +3 -1
  24. canns/pipeline/asa_gui/core/runner.py +23 -23
  25. canns/pipeline/asa_gui/views/pages/preprocess_page.py +7 -12
  26. {canns-0.14.3.dist-info → canns-0.15.1.dist-info}/METADATA +1 -1
  27. {canns-0.14.3.dist-info → canns-0.15.1.dist-info}/RECORD +30 -23
  28. canns/analyzer/data/asa/filters.py +0 -208
  29. {canns-0.14.3.dist-info → canns-0.15.1.dist-info}/WHEEL +0 -0
  30. {canns-0.14.3.dist-info → canns-0.15.1.dist-info}/entry_points.txt +0 -0
  31. {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