canns 0.13.1__py3-none-any.whl → 0.14.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.
- canns/analyzer/data/__init__.py +5 -1
- canns/analyzer/data/asa/__init__.py +27 -12
- canns/analyzer/data/asa/cohospace.py +336 -10
- canns/analyzer/data/asa/config.py +3 -0
- canns/analyzer/data/asa/embedding.py +48 -45
- canns/analyzer/data/asa/path.py +104 -2
- canns/analyzer/data/asa/plotting.py +88 -19
- canns/analyzer/data/asa/tda.py +11 -4
- canns/analyzer/data/cell_classification/__init__.py +97 -0
- canns/analyzer/data/cell_classification/core/__init__.py +26 -0
- canns/analyzer/data/cell_classification/core/grid_cells.py +633 -0
- canns/analyzer/data/cell_classification/core/grid_modules_leiden.py +288 -0
- canns/analyzer/data/cell_classification/core/head_direction.py +347 -0
- canns/analyzer/data/cell_classification/core/spatial_analysis.py +431 -0
- canns/analyzer/data/cell_classification/io/__init__.py +5 -0
- canns/analyzer/data/cell_classification/io/matlab_loader.py +417 -0
- canns/analyzer/data/cell_classification/utils/__init__.py +39 -0
- canns/analyzer/data/cell_classification/utils/circular_stats.py +383 -0
- canns/analyzer/data/cell_classification/utils/correlation.py +318 -0
- canns/analyzer/data/cell_classification/utils/geometry.py +442 -0
- canns/analyzer/data/cell_classification/utils/image_processing.py +416 -0
- canns/analyzer/data/cell_classification/visualization/__init__.py +19 -0
- canns/analyzer/data/cell_classification/visualization/grid_plots.py +292 -0
- canns/analyzer/data/cell_classification/visualization/hd_plots.py +200 -0
- canns/analyzer/metrics/__init__.py +2 -1
- canns/analyzer/visualization/core/config.py +46 -4
- canns/data/__init__.py +6 -1
- canns/data/datasets.py +154 -1
- canns/data/loaders.py +37 -0
- canns/pipeline/__init__.py +13 -9
- canns/pipeline/__main__.py +6 -0
- canns/pipeline/asa/runner.py +105 -41
- canns/pipeline/asa_gui/__init__.py +68 -0
- canns/pipeline/asa_gui/__main__.py +6 -0
- canns/pipeline/asa_gui/analysis_modes/__init__.py +42 -0
- canns/pipeline/asa_gui/analysis_modes/base.py +39 -0
- canns/pipeline/asa_gui/analysis_modes/batch_mode.py +21 -0
- canns/pipeline/asa_gui/analysis_modes/cohomap_mode.py +56 -0
- canns/pipeline/asa_gui/analysis_modes/cohospace_mode.py +194 -0
- canns/pipeline/asa_gui/analysis_modes/decode_mode.py +52 -0
- canns/pipeline/asa_gui/analysis_modes/fr_mode.py +81 -0
- canns/pipeline/asa_gui/analysis_modes/frm_mode.py +92 -0
- canns/pipeline/asa_gui/analysis_modes/gridscore_mode.py +123 -0
- canns/pipeline/asa_gui/analysis_modes/pathcompare_mode.py +199 -0
- canns/pipeline/asa_gui/analysis_modes/tda_mode.py +112 -0
- canns/pipeline/asa_gui/app.py +29 -0
- canns/pipeline/asa_gui/controllers/__init__.py +6 -0
- canns/pipeline/asa_gui/controllers/analysis_controller.py +59 -0
- canns/pipeline/asa_gui/controllers/preprocess_controller.py +89 -0
- canns/pipeline/asa_gui/core/__init__.py +15 -0
- canns/pipeline/asa_gui/core/cache.py +14 -0
- canns/pipeline/asa_gui/core/runner.py +1936 -0
- canns/pipeline/asa_gui/core/state.py +324 -0
- canns/pipeline/asa_gui/core/worker.py +260 -0
- canns/pipeline/asa_gui/main_window.py +184 -0
- canns/pipeline/asa_gui/models/__init__.py +7 -0
- canns/pipeline/asa_gui/models/config.py +14 -0
- canns/pipeline/asa_gui/models/job.py +31 -0
- canns/pipeline/asa_gui/models/presets.py +21 -0
- canns/pipeline/asa_gui/resources/__init__.py +16 -0
- canns/pipeline/asa_gui/resources/dark.qss +167 -0
- canns/pipeline/asa_gui/resources/light.qss +163 -0
- canns/pipeline/asa_gui/resources/styles.qss +130 -0
- canns/pipeline/asa_gui/utils/__init__.py +1 -0
- canns/pipeline/asa_gui/utils/formatters.py +15 -0
- canns/pipeline/asa_gui/utils/io_adapters.py +40 -0
- canns/pipeline/asa_gui/utils/validators.py +41 -0
- canns/pipeline/asa_gui/views/__init__.py +1 -0
- canns/pipeline/asa_gui/views/help_content.py +171 -0
- canns/pipeline/asa_gui/views/pages/__init__.py +6 -0
- canns/pipeline/asa_gui/views/pages/analysis_page.py +565 -0
- canns/pipeline/asa_gui/views/pages/preprocess_page.py +492 -0
- canns/pipeline/asa_gui/views/panels/__init__.py +1 -0
- canns/pipeline/asa_gui/views/widgets/__init__.py +21 -0
- canns/pipeline/asa_gui/views/widgets/artifacts_tab.py +44 -0
- canns/pipeline/asa_gui/views/widgets/drop_zone.py +80 -0
- canns/pipeline/asa_gui/views/widgets/file_list.py +27 -0
- canns/pipeline/asa_gui/views/widgets/gridscore_tab.py +308 -0
- canns/pipeline/asa_gui/views/widgets/help_dialog.py +27 -0
- canns/pipeline/asa_gui/views/widgets/image_tab.py +50 -0
- canns/pipeline/asa_gui/views/widgets/image_viewer.py +97 -0
- canns/pipeline/asa_gui/views/widgets/log_box.py +16 -0
- canns/pipeline/asa_gui/views/widgets/pathcompare_tab.py +200 -0
- canns/pipeline/asa_gui/views/widgets/popup_combo.py +25 -0
- canns/pipeline/gallery/__init__.py +15 -5
- canns/pipeline/gallery/__main__.py +11 -0
- canns/pipeline/gallery/app.py +705 -0
- canns/pipeline/gallery/runner.py +790 -0
- canns/pipeline/gallery/state.py +51 -0
- canns/pipeline/gallery/styles.tcss +123 -0
- canns/pipeline/launcher.py +81 -0
- {canns-0.13.1.dist-info → canns-0.14.0.dist-info}/METADATA +11 -1
- canns-0.14.0.dist-info/RECORD +163 -0
- canns-0.14.0.dist-info/entry_points.txt +5 -0
- canns/pipeline/_base.py +0 -50
- canns-0.13.1.dist-info/RECORD +0 -89
- canns-0.13.1.dist-info/entry_points.txt +0 -3
- {canns-0.13.1.dist-info → canns-0.14.0.dist-info}/WHEEL +0 -0
- {canns-0.13.1.dist-info → canns-0.14.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geometry Utilities
|
|
3
|
+
|
|
4
|
+
Functions for geometric calculations including ellipse fitting,
|
|
5
|
+
distance computations, and polygon operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def fit_ellipse(x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
|
12
|
+
"""
|
|
13
|
+
Least-squares fit of ellipse to 2D points.
|
|
14
|
+
|
|
15
|
+
Implements the Direct Least Squares Fitting algorithm by Fitzgibbon et al. (1999).
|
|
16
|
+
This is a robust method that includes scaling to reduce roundoff error and
|
|
17
|
+
returns geometric parameters rather than quadratic form coefficients.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
x : np.ndarray
|
|
22
|
+
X coordinates of points (1D array)
|
|
23
|
+
y : np.ndarray
|
|
24
|
+
Y coordinates of points (1D array)
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
params : np.ndarray
|
|
29
|
+
Array of shape (5,) containing:
|
|
30
|
+
[center_x, center_y, radius_x, radius_y, theta_radians]
|
|
31
|
+
where theta is the orientation angle of the major axis
|
|
32
|
+
|
|
33
|
+
Examples
|
|
34
|
+
--------
|
|
35
|
+
>>> # Generate points on an ellipse
|
|
36
|
+
>>> t = np.linspace(0, 2*np.pi, 100)
|
|
37
|
+
>>> cx, cy = 5, 3 # center
|
|
38
|
+
>>> rx, ry = 4, 2 # radii
|
|
39
|
+
>>> angle = np.pi/4 # rotation
|
|
40
|
+
>>> x = cx + rx * np.cos(t) * np.cos(angle) - ry * np.sin(t) * np.sin(angle)
|
|
41
|
+
>>> y = cy + rx * np.cos(t) * np.sin(angle) + ry * np.sin(t) * np.cos(angle)
|
|
42
|
+
>>> params = fit_ellipse(x, y)
|
|
43
|
+
>>> print(f"Fitted center: ({params[0]:.2f}, {params[1]:.2f})")
|
|
44
|
+
>>> print(f"Fitted radii: ({params[2]:.2f}, {params[3]:.2f})")
|
|
45
|
+
|
|
46
|
+
Notes
|
|
47
|
+
-----
|
|
48
|
+
Based on fitEllipse.m from the MATLAB codebase.
|
|
49
|
+
|
|
50
|
+
References
|
|
51
|
+
----------
|
|
52
|
+
Fitzgibbon, A.W., Pilu, M., and Fisher, R.B. (1999).
|
|
53
|
+
"Direct least-squares fitting of ellipses". IEEE T-PAMI, 21(5):476-480.
|
|
54
|
+
http://research.microsoft.com/en-us/um/people/awf/ellipse/
|
|
55
|
+
"""
|
|
56
|
+
x = np.asarray(x, dtype=float)
|
|
57
|
+
y = np.asarray(y, dtype=float)
|
|
58
|
+
|
|
59
|
+
if len(x) < 5 or len(y) < 5:
|
|
60
|
+
raise ValueError("Need at least 5 points to fit an ellipse")
|
|
61
|
+
|
|
62
|
+
# Normalize data to reduce roundoff error
|
|
63
|
+
mx = np.mean(x)
|
|
64
|
+
my = np.mean(y)
|
|
65
|
+
sx = (np.max(x) - np.min(x)) / 2
|
|
66
|
+
sy = (np.max(y) - np.min(y)) / 2
|
|
67
|
+
|
|
68
|
+
if sx == 0 or sy == 0:
|
|
69
|
+
raise ValueError("Points must have non-zero extent in both dimensions")
|
|
70
|
+
|
|
71
|
+
x_norm = (x - mx) / sx
|
|
72
|
+
y_norm = (y - my) / sy
|
|
73
|
+
|
|
74
|
+
# Force to column vectors
|
|
75
|
+
x_norm = x_norm.ravel()
|
|
76
|
+
y_norm = y_norm.ravel()
|
|
77
|
+
|
|
78
|
+
# Build design matrix
|
|
79
|
+
# D = [x^2, xy, y^2, x, y, 1]
|
|
80
|
+
D = np.column_stack(
|
|
81
|
+
[x_norm**2, x_norm * y_norm, y_norm**2, x_norm, y_norm, np.ones_like(x_norm)]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Build scatter matrix
|
|
85
|
+
S = D.T @ D
|
|
86
|
+
|
|
87
|
+
# Build 6x6 constraint matrix for ellipse
|
|
88
|
+
# This constrains the solution to be an ellipse (not hyperbola or parabola)
|
|
89
|
+
C = np.zeros((6, 6))
|
|
90
|
+
C[0, 2] = -2
|
|
91
|
+
C[1, 1] = 1
|
|
92
|
+
C[2, 0] = -2
|
|
93
|
+
|
|
94
|
+
# Solve the generalized eigensystem using the stable method
|
|
95
|
+
# Break into blocks (as in the "new way" from the MATLAB code)
|
|
96
|
+
tmpA = S[:3, :3]
|
|
97
|
+
tmpB = S[:3, 3:6]
|
|
98
|
+
tmpC = S[3:6, 3:6]
|
|
99
|
+
tmpD = C[:3, :3]
|
|
100
|
+
|
|
101
|
+
# Solve the reduced eigensystem
|
|
102
|
+
try:
|
|
103
|
+
tmpE = np.linalg.inv(tmpC) @ tmpB.T
|
|
104
|
+
tmpM = np.linalg.inv(tmpD) @ (tmpA - tmpB @ tmpE)
|
|
105
|
+
|
|
106
|
+
# Find eigenvalues and eigenvectors
|
|
107
|
+
eigenvalues, eigenvectors = np.linalg.eig(tmpM)
|
|
108
|
+
|
|
109
|
+
# Find the positive eigenvalue (since det(tmpD) < 0)
|
|
110
|
+
# Look for small positive or negative eigenvalues near zero
|
|
111
|
+
idx = np.where((np.real(eigenvalues) < 1e-8) & ~np.isinf(eigenvalues))[0]
|
|
112
|
+
|
|
113
|
+
if len(idx) == 0:
|
|
114
|
+
# Fallback: take the smallest positive eigenvalue
|
|
115
|
+
idx = np.argmin(np.abs(eigenvalues))
|
|
116
|
+
else:
|
|
117
|
+
idx = idx[0]
|
|
118
|
+
|
|
119
|
+
# Extract eigenvector
|
|
120
|
+
A_top = np.real(eigenvectors[:, idx])
|
|
121
|
+
|
|
122
|
+
# Recover the bottom half
|
|
123
|
+
A_bottom = -tmpE @ A_top
|
|
124
|
+
A = np.concatenate([A_top, A_bottom])
|
|
125
|
+
|
|
126
|
+
except np.linalg.LinAlgError as err:
|
|
127
|
+
raise ValueError("Failed to fit ellipse: singular matrix encountered") from err
|
|
128
|
+
|
|
129
|
+
# Unnormalize the coefficients
|
|
130
|
+
par = np.array(
|
|
131
|
+
[
|
|
132
|
+
A[0] * sy * sy,
|
|
133
|
+
A[1] * sx * sy,
|
|
134
|
+
A[2] * sx * sx,
|
|
135
|
+
-2 * A[0] * sy * sy * mx - A[1] * sx * sy * my + A[3] * sx * sy * sy,
|
|
136
|
+
-A[1] * sx * sy * mx - 2 * A[2] * sx * sx * my + A[4] * sx * sx * sy,
|
|
137
|
+
A[0] * sy * sy * mx * mx
|
|
138
|
+
+ A[1] * sx * sy * mx * my
|
|
139
|
+
+ A[2] * sx * sx * my * my
|
|
140
|
+
- A[3] * sx * sy * sy * mx
|
|
141
|
+
- A[4] * sx * sx * sy * my
|
|
142
|
+
+ A[5] * sx * sx * sy * sy,
|
|
143
|
+
]
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Convert quadratic form to geometric parameters
|
|
147
|
+
theta_rad = 0.5 * np.arctan2(par[1], par[0] - par[2])
|
|
148
|
+
cost = np.cos(theta_rad)
|
|
149
|
+
sint = np.sin(theta_rad)
|
|
150
|
+
sin_squared = sint**2
|
|
151
|
+
cos_squared = cost**2
|
|
152
|
+
cos_sin = sint * cost
|
|
153
|
+
|
|
154
|
+
Ao = par[5]
|
|
155
|
+
Au = par[3] * cost + par[4] * sint
|
|
156
|
+
Av = -par[3] * sint + par[4] * cost
|
|
157
|
+
Auu = par[0] * cos_squared + par[2] * sin_squared + par[1] * cos_sin
|
|
158
|
+
Avv = par[0] * sin_squared + par[2] * cos_squared - par[1] * cos_sin
|
|
159
|
+
|
|
160
|
+
# Center in rotated coordinates
|
|
161
|
+
tuCentre = -Au / (2 * Auu)
|
|
162
|
+
tvCentre = -Av / (2 * Avv)
|
|
163
|
+
wCentre = Ao - Auu * tuCentre**2 - Avv * tvCentre**2
|
|
164
|
+
|
|
165
|
+
# Transform back to original coordinates
|
|
166
|
+
uCentre = tuCentre * cost - tvCentre * sint
|
|
167
|
+
vCentre = tuCentre * sint + tvCentre * cost
|
|
168
|
+
|
|
169
|
+
# Radii
|
|
170
|
+
Ru = -wCentre / Auu
|
|
171
|
+
Rv = -wCentre / Avv
|
|
172
|
+
|
|
173
|
+
Ru = np.sqrt(np.abs(Ru)) * np.sign(Ru)
|
|
174
|
+
Rv = np.sqrt(np.abs(Rv)) * np.sign(Rv)
|
|
175
|
+
|
|
176
|
+
# Return geometric parameters
|
|
177
|
+
result = np.array([uCentre, vCentre, Ru, Rv, theta_rad])
|
|
178
|
+
|
|
179
|
+
return result
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def squared_distance(X: np.ndarray, Y: np.ndarray) -> np.ndarray:
|
|
183
|
+
"""
|
|
184
|
+
Compute squared Euclidean distance matrix between two sets of points.
|
|
185
|
+
|
|
186
|
+
Efficiently computes all pairwise squared distances between points in X and Y
|
|
187
|
+
using the identity: ||x - y||^2 = ||x||^2 + ||y||^2 - 2*x·y
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
X : np.ndarray
|
|
192
|
+
First set of points, shape (d, n) where d is dimension, n is number of points
|
|
193
|
+
Y : np.ndarray
|
|
194
|
+
Second set of points, shape (d, m) where d is dimension, m is number of points
|
|
195
|
+
|
|
196
|
+
Returns
|
|
197
|
+
-------
|
|
198
|
+
D : np.ndarray
|
|
199
|
+
Squared distance matrix, shape (n, m)
|
|
200
|
+
D[i, j] = ||X[:, i] - Y[:, j]||^2
|
|
201
|
+
|
|
202
|
+
Examples
|
|
203
|
+
--------
|
|
204
|
+
>>> # 2D points
|
|
205
|
+
>>> X = np.array([[0, 1, 2], [0, 0, 0]]) # 3 points along x-axis
|
|
206
|
+
>>> Y = np.array([[0, 0], [1, 2]]) # 2 points along y-axis
|
|
207
|
+
>>> D = squared_distance(X, Y)
|
|
208
|
+
>>> print(D) # Distances from X points to Y points
|
|
209
|
+
|
|
210
|
+
Notes
|
|
211
|
+
-----
|
|
212
|
+
Based on sqDistance inline function from gridnessScore.m and findCentreRadius.m.
|
|
213
|
+
Uses bsxfun-style broadcasting for efficiency.
|
|
214
|
+
"""
|
|
215
|
+
X = np.atleast_2d(X)
|
|
216
|
+
Y = np.atleast_2d(Y)
|
|
217
|
+
|
|
218
|
+
# ||x||^2 for each column of X
|
|
219
|
+
X_norm_sq = np.sum(X**2, axis=0, keepdims=True) # Shape: (1, n)
|
|
220
|
+
|
|
221
|
+
# ||y||^2 for each column of Y
|
|
222
|
+
Y_norm_sq = np.sum(Y**2, axis=0, keepdims=True) # Shape: (1, m)
|
|
223
|
+
|
|
224
|
+
# Compute: ||x||^2 + ||y||^2 - 2*x·y using broadcasting
|
|
225
|
+
# X_norm_sq.T: (n, 1)
|
|
226
|
+
# Y_norm_sq: (1, m)
|
|
227
|
+
# X.T @ Y: (n, m)
|
|
228
|
+
D = X_norm_sq.T + Y_norm_sq - 2 * (X.T @ Y)
|
|
229
|
+
|
|
230
|
+
# Ensure non-negative (due to floating point errors)
|
|
231
|
+
D = np.maximum(D, 0)
|
|
232
|
+
|
|
233
|
+
return D
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def polyarea(x: np.ndarray, y: np.ndarray) -> float:
|
|
237
|
+
"""
|
|
238
|
+
Compute area of a polygon using the shoelace formula.
|
|
239
|
+
|
|
240
|
+
Parameters
|
|
241
|
+
----------
|
|
242
|
+
x : np.ndarray
|
|
243
|
+
X coordinates of polygon vertices
|
|
244
|
+
y : np.ndarray
|
|
245
|
+
Y coordinates of polygon vertices
|
|
246
|
+
|
|
247
|
+
Returns
|
|
248
|
+
-------
|
|
249
|
+
area : float
|
|
250
|
+
Area of the polygon (always positive)
|
|
251
|
+
|
|
252
|
+
Examples
|
|
253
|
+
--------
|
|
254
|
+
>>> # Unit square
|
|
255
|
+
>>> x = np.array([0, 1, 1, 0])
|
|
256
|
+
>>> y = np.array([0, 0, 1, 1])
|
|
257
|
+
>>> area = polyarea(x, y)
|
|
258
|
+
>>> print(f"Area: {area}") # Should be 1.0
|
|
259
|
+
|
|
260
|
+
>>> # Triangle
|
|
261
|
+
>>> x = np.array([0, 1, 0.5])
|
|
262
|
+
>>> y = np.array([0, 0, 1])
|
|
263
|
+
>>> area = polyarea(x, y)
|
|
264
|
+
>>> print(f"Area: {area}") # Should be 0.5
|
|
265
|
+
|
|
266
|
+
Notes
|
|
267
|
+
-----
|
|
268
|
+
Based on MATLAB's polyarea function using the shoelace formula.
|
|
269
|
+
The polygon can be specified in either clockwise or counter-clockwise order.
|
|
270
|
+
"""
|
|
271
|
+
x = np.asarray(x).ravel()
|
|
272
|
+
y = np.asarray(y).ravel()
|
|
273
|
+
|
|
274
|
+
if len(x) != len(y):
|
|
275
|
+
raise ValueError("x and y must have the same length")
|
|
276
|
+
|
|
277
|
+
if len(x) < 3:
|
|
278
|
+
return 0.0
|
|
279
|
+
|
|
280
|
+
# Shoelace formula: A = 0.5 * |sum(x_i * y_{i+1} - x_{i+1} * y_i)|
|
|
281
|
+
# Use np.roll to get next elements cyclically
|
|
282
|
+
area = 0.5 * np.abs(np.sum(x * np.roll(y, -1)) - np.sum(np.roll(x, -1) * y))
|
|
283
|
+
|
|
284
|
+
return float(area)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def wrap_to_pi(angles: np.ndarray) -> np.ndarray:
|
|
288
|
+
"""
|
|
289
|
+
Wrap angles to the range [-π, π].
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
angles : np.ndarray
|
|
294
|
+
Angles in radians
|
|
295
|
+
|
|
296
|
+
Returns
|
|
297
|
+
-------
|
|
298
|
+
wrapped : np.ndarray
|
|
299
|
+
Angles wrapped to [-π, π]
|
|
300
|
+
|
|
301
|
+
Examples
|
|
302
|
+
--------
|
|
303
|
+
>>> angles = np.array([0, np.pi, -np.pi, 3*np.pi, -3*np.pi])
|
|
304
|
+
>>> wrapped = wrap_to_pi(angles)
|
|
305
|
+
>>> print(wrapped) # All in [-π, π]
|
|
306
|
+
|
|
307
|
+
Notes
|
|
308
|
+
-----
|
|
309
|
+
Equivalent to MATLAB's wrapToPi function.
|
|
310
|
+
"""
|
|
311
|
+
return np.arctan2(np.sin(angles), np.cos(angles))
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def cart2pol(x: np.ndarray, y: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
315
|
+
"""
|
|
316
|
+
Transform Cartesian coordinates to polar coordinates.
|
|
317
|
+
|
|
318
|
+
Parameters
|
|
319
|
+
----------
|
|
320
|
+
x : np.ndarray
|
|
321
|
+
X coordinates
|
|
322
|
+
y : np.ndarray
|
|
323
|
+
Y coordinates
|
|
324
|
+
|
|
325
|
+
Returns
|
|
326
|
+
-------
|
|
327
|
+
theta : np.ndarray
|
|
328
|
+
Angle in radians, range [-π, π]
|
|
329
|
+
rho : np.ndarray
|
|
330
|
+
Radius (distance from origin)
|
|
331
|
+
|
|
332
|
+
Examples
|
|
333
|
+
--------
|
|
334
|
+
>>> x = np.array([1, 0, -1])
|
|
335
|
+
>>> y = np.array([0, 1, 0])
|
|
336
|
+
>>> theta, rho = cart2pol(x, y)
|
|
337
|
+
>>> print(theta) # [0, π/2, π]
|
|
338
|
+
>>> print(rho) # [1, 1, 1]
|
|
339
|
+
|
|
340
|
+
Notes
|
|
341
|
+
-----
|
|
342
|
+
Equivalent to MATLAB's cart2pol function.
|
|
343
|
+
"""
|
|
344
|
+
theta = np.arctan2(y, x)
|
|
345
|
+
rho = np.sqrt(x**2 + y**2)
|
|
346
|
+
return theta, rho
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def pol2cart(theta: np.ndarray, rho: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
350
|
+
"""
|
|
351
|
+
Transform polar coordinates to Cartesian coordinates.
|
|
352
|
+
|
|
353
|
+
Parameters
|
|
354
|
+
----------
|
|
355
|
+
theta : np.ndarray
|
|
356
|
+
Angle in radians
|
|
357
|
+
rho : np.ndarray
|
|
358
|
+
Radius (distance from origin)
|
|
359
|
+
|
|
360
|
+
Returns
|
|
361
|
+
-------
|
|
362
|
+
x : np.ndarray
|
|
363
|
+
X coordinates
|
|
364
|
+
y : np.ndarray
|
|
365
|
+
Y coordinates
|
|
366
|
+
|
|
367
|
+
Examples
|
|
368
|
+
--------
|
|
369
|
+
>>> theta = np.array([0, np.pi/2, np.pi])
|
|
370
|
+
>>> rho = np.array([1, 1, 1])
|
|
371
|
+
>>> x, y = pol2cart(theta, rho)
|
|
372
|
+
>>> print(x) # [1, 0, -1]
|
|
373
|
+
>>> print(y) # [0, 1, 0]
|
|
374
|
+
|
|
375
|
+
Notes
|
|
376
|
+
-----
|
|
377
|
+
Equivalent to MATLAB's pol2cart function.
|
|
378
|
+
"""
|
|
379
|
+
x = rho * np.cos(theta)
|
|
380
|
+
y = rho * np.sin(theta)
|
|
381
|
+
return x, y
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
if __name__ == "__main__":
|
|
385
|
+
# Simple tests
|
|
386
|
+
print("Testing geometry functions...")
|
|
387
|
+
|
|
388
|
+
# Test 1: Fit ellipse
|
|
389
|
+
print("\nTest 1 - Ellipse fitting:")
|
|
390
|
+
t = np.linspace(0, 2 * np.pi, 100)
|
|
391
|
+
cx, cy = 5, 3 # center
|
|
392
|
+
rx, ry = 4, 2 # radii
|
|
393
|
+
angle = np.pi / 6 # rotation
|
|
394
|
+
|
|
395
|
+
# Generate points on ellipse
|
|
396
|
+
x = cx + rx * np.cos(t) * np.cos(angle) - ry * np.sin(t) * np.sin(angle)
|
|
397
|
+
y = cy + rx * np.cos(t) * np.sin(angle) + ry * np.sin(t) * np.cos(angle)
|
|
398
|
+
|
|
399
|
+
params = fit_ellipse(x, y)
|
|
400
|
+
print(f" True center: ({cx}, {cy})")
|
|
401
|
+
print(f" Fitted center: ({params[0]:.2f}, {params[1]:.2f})")
|
|
402
|
+
print(f" True radii: ({rx}, {ry})")
|
|
403
|
+
print(f" Fitted radii: ({abs(params[2]):.2f}, {abs(params[3]):.2f})")
|
|
404
|
+
print(f" True angle: {np.rad2deg(angle):.2f}°")
|
|
405
|
+
print(f" Fitted angle: {np.rad2deg(params[4]):.2f}°")
|
|
406
|
+
|
|
407
|
+
# Test 2: Squared distance
|
|
408
|
+
print("\nTest 2 - Squared distance:")
|
|
409
|
+
X = np.array([[0, 1, 2], [0, 0, 0]]) # 3 points along x-axis
|
|
410
|
+
Y = np.array([[0, 0], [1, 2]]) # 2 points along y-axis
|
|
411
|
+
D = squared_distance(X, Y)
|
|
412
|
+
print(f" X points: {X.T}")
|
|
413
|
+
print(f" Y points: {Y.T}")
|
|
414
|
+
print(f" Squared distances:\n{D}")
|
|
415
|
+
|
|
416
|
+
# Test 3: Polygon area
|
|
417
|
+
print("\nTest 3 - Polygon area:")
|
|
418
|
+
# Unit square
|
|
419
|
+
x = np.array([0, 1, 1, 0])
|
|
420
|
+
y = np.array([0, 0, 1, 1])
|
|
421
|
+
area = polyarea(x, y)
|
|
422
|
+
print(f" Unit square area: {area:.3f} (should be 1.0)")
|
|
423
|
+
|
|
424
|
+
# Triangle
|
|
425
|
+
x = np.array([0, 1, 0.5])
|
|
426
|
+
y = np.array([0, 0, 1])
|
|
427
|
+
area = polyarea(x, y)
|
|
428
|
+
print(f" Triangle area: {area:.3f} (should be 0.5)")
|
|
429
|
+
|
|
430
|
+
# Test 4: Coordinate transformations
|
|
431
|
+
print("\nTest 4 - Coordinate transformations:")
|
|
432
|
+
x = np.array([1, 0, -1, 0])
|
|
433
|
+
y = np.array([0, 1, 0, -1])
|
|
434
|
+
theta, rho = cart2pol(x, y)
|
|
435
|
+
print(f" Cartesian: {np.column_stack([x, y])}")
|
|
436
|
+
print(f" Polar (θ, ρ): {np.column_stack([np.rad2deg(theta), rho])}")
|
|
437
|
+
|
|
438
|
+
x2, y2 = pol2cart(theta, rho)
|
|
439
|
+
print(f" Back to Cartesian: {np.column_stack([x2, y2])}")
|
|
440
|
+
print(f" Error: {np.max(np.abs(np.column_stack([x - x2, y - y2]))):.10f}")
|
|
441
|
+
|
|
442
|
+
print("\nAll tests completed!")
|