morphomatics 4.0__tar.gz → 4.1__tar.gz

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 (69) hide show
  1. {morphomatics-4.0 → morphomatics-4.1}/PKG-INFO +14 -2
  2. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/__init__.py +1 -1
  3. morphomatics-4.1/morphomatics/correspondence/__init__.py +15 -0
  4. morphomatics-4.1/morphomatics/correspondence/convert.py +81 -0
  5. morphomatics-4.1/morphomatics/correspondence/laplacian.py +61 -0
  6. morphomatics-4.1/morphomatics/correspondence/refine.py +88 -0
  7. morphomatics-4.1/morphomatics/correspondence/util.py +356 -0
  8. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/geom/__init__.py +1 -1
  9. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/geom/bezier_spline.py +1 -1
  10. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/geom/misc.py +5 -2
  11. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/geom/surface.py +28 -14
  12. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/graph/__init__.py +1 -1
  13. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/graph/operators.py +1 -1
  14. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/__init__.py +4 -1
  15. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/bezierfold.py +9 -16
  16. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/connection.py +1 -1
  17. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/cubic_bezierfold.py +1 -8
  18. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/differential_coords.py +1 -34
  19. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/discrete_ops.py +1 -1
  20. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/euclidean.py +4 -31
  21. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/fundamental_coords.py +1 -34
  22. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/gl_p_coords.py +3 -6
  23. morphomatics-4.1/morphomatics/manifold/gl_p_n.py +196 -0
  24. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/grassmann.py +16 -15
  25. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/hyperbolic_space.py +1 -8
  26. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/kendall.py +5 -17
  27. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/lie_group.py +27 -20
  28. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/manifold.py +1 -36
  29. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/manopt_wrapper.py +1 -1
  30. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/metric.py +34 -8
  31. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/point_distribution_model.py +3 -12
  32. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/power_manifold.py +60 -61
  33. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/product_manifold.py +25 -35
  34. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/se_3.py +63 -139
  35. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/shape_space.py +1 -1
  36. morphomatics-4.1/morphomatics/manifold/simplex.py +207 -0
  37. morphomatics-4.1/morphomatics/manifold/size_and_shape.py +199 -0
  38. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/so_3.py +32 -95
  39. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/spd.py +16 -57
  40. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/sphere.py +6 -9
  41. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/tangent_bundle.py +1 -8
  42. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/util.py +62 -1
  43. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/nn/__init__.py +2 -1
  44. morphomatics-4.1/morphomatics/nn/euclidean_layers.py +36 -0
  45. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/nn/flow_layers.py +31 -19
  46. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/nn/tangent_layers.py +1 -1
  47. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/nn/wFM_layers.py +1 -1
  48. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/opt/__init__.py +1 -1
  49. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/opt/riemannian_newton_raphson.py +1 -1
  50. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/opt/riemannian_steepest_descent.py +6 -5
  51. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/stats/__init__.py +3 -2
  52. morphomatics-4.0/morphomatics/stats/biinvariant_statistics.py → morphomatics-4.1/morphomatics/stats/biinvariant_dissimilarity_measures.py +8 -5
  53. morphomatics-4.1/morphomatics/stats/biinvariant_regression.py +203 -0
  54. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/stats/exponential_barycenter.py +1 -1
  55. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/stats/geometric_median.py +1 -1
  56. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/stats/principal_geodesic_analysis.py +1 -1
  57. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/stats/riemannian_regression.py +1 -1
  58. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/stats/statistical_shape_model.py +1 -1
  59. {morphomatics-4.0 → morphomatics-4.1}/morphomatics.egg-info/PKG-INFO +14 -2
  60. {morphomatics-4.0 → morphomatics-4.1}/morphomatics.egg-info/SOURCES.txt +10 -1
  61. {morphomatics-4.0 → morphomatics-4.1}/setup.py +3 -2
  62. morphomatics-4.0/morphomatics/manifold/gl_p_n.py +0 -201
  63. {morphomatics-4.0 → morphomatics-4.1}/LICENSE +0 -0
  64. {morphomatics-4.0 → morphomatics-4.1}/README.md +0 -0
  65. {morphomatics-4.0 → morphomatics-4.1}/morphomatics/nn/train.py +0 -0
  66. {morphomatics-4.0 → morphomatics-4.1}/morphomatics.egg-info/dependency_links.txt +0 -0
  67. {morphomatics-4.0 → morphomatics-4.1}/morphomatics.egg-info/requires.txt +0 -0
  68. {morphomatics-4.0 → morphomatics-4.1}/morphomatics.egg-info/top_level.txt +0 -0
  69. {morphomatics-4.0 → morphomatics-4.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: morphomatics
3
- Version: 4.0
3
+ Version: 4.1
4
4
  Summary: Geometric morphometrics in non-Euclidean shape spaces
5
5
  Home-page: https://morphomatics.github.io/
6
6
  Author: Christoph von Tycowicz et al.
@@ -24,6 +24,18 @@ Requires-Dist: flax
24
24
  Requires-Dist: optax
25
25
  Provides-Extra: all
26
26
  Requires-Dist: pymanopt>=2.0.1; extra == "all"
27
+ Dynamic: author
28
+ Dynamic: author-email
29
+ Dynamic: classifier
30
+ Dynamic: description
31
+ Dynamic: description-content-type
32
+ Dynamic: home-page
33
+ Dynamic: keywords
34
+ Dynamic: license
35
+ Dynamic: license-file
36
+ Dynamic: provides-extra
37
+ Dynamic: requires-dist
38
+ Dynamic: summary
27
39
 
28
40
  <div align="center">
29
41
  <img src="https://github.com/morphomatics/morphomatics.github.io/blob/master/images/logo_cyan.png?raw=true" width="250" alt="Morphomatics"/>
@@ -10,4 +10,4 @@
10
10
  # #
11
11
  ################################################################################
12
12
 
13
- __version__ = '4.0.dev0'
13
+ __version__ = '4.1'
@@ -0,0 +1,15 @@
1
+ ################################################################################
2
+ # #
3
+ # This file is part of the Morphomatics library #
4
+ # see https://github.com/morphomatics/morphomatics #
5
+ # #
6
+ # Copyright (C) 2025 Zuse Institute Berlin #
7
+ # #
8
+ # Morphomatics is distributed under the terms of the MIT License. #
9
+ # see $MORPHOMATICS/LICENSE #
10
+ # #
11
+ ################################################################################
12
+
13
+ from .laplacian import *
14
+ from .convert import *
15
+ from .refine import *
@@ -0,0 +1,81 @@
1
+ ################################################################################
2
+ # #
3
+ # This file is part of the Morphomatics library #
4
+ # see https://github.com/morphomatics/morphomatics #
5
+ # #
6
+ # Copyright (C) 2025 Zuse Institute Berlin #
7
+ # #
8
+ # Morphomatics is distributed under the terms of the MIT License. #
9
+ # see $MORPHOMATICS/LICENSE #
10
+ # #
11
+ ################################################################################
12
+
13
+ from scipy import sparse
14
+ from scipy import spatial
15
+ from sklearn.neighbors import NearestNeighbors, KDTree
16
+ from .util import *
17
+
18
+
19
+ def to_hat(bases, C):
20
+ Phi1, Phi2 = bases[0], bases[1]
21
+ n1, n2 = Phi1.shape[0], Phi2.shape[0]
22
+ nn = NearestNeighbors(n_neighbors=1, n_jobs=-1).fit(Phi1)
23
+ inds = nn.kneighbors(Phi2 @ C, return_distance=False)
24
+ inds = inds.squeeze()
25
+ return sparse.csr_matrix((np.ones(n2), (np.arange(n2), inds)), shape=(n2, n1))
26
+
27
+
28
+ def to_hat_iso(bases, C):
29
+ Phi1, Phi2 = bases[0], bases[1]
30
+ n1, n2 = Phi1.shape[0], Phi2.shape[0]
31
+ nn = NearestNeighbors(n_neighbors=1, n_jobs=-1).fit(Phi1 @ C.T)
32
+ inds = nn.kneighbors(Phi2, return_distance=False)
33
+ inds = inds.squeeze()
34
+ return sparse.csr_matrix((np.ones(n2), (np.arange(n2), inds)), shape=(n2, n1))
35
+
36
+
37
+ def to_precise(ops, C):
38
+ """ Convert map of functionwise correspondence to precise map """
39
+ s1, s2 = ops[0].surf, ops[1].surf
40
+ n1 = s1.v.shape[0]
41
+ n2 = s2.v.shape[0]
42
+ Phi1, Phi2 = ops[0].evecs, ops[1].evecs
43
+ Phi1 = Phi1[:, :C.shape[1]]
44
+ Phi2 = Phi2[:, :C.shape[0]]
45
+ Phi1_c0 = Phi1[s1.f[:, 0], :]
46
+ Phi1_c1 = Phi1[s1.f[:, 1], :]
47
+ Phi1_c2 = Phi1[s1.f[:, 2], :]
48
+ # compute l_max
49
+ norm_c1c0 = np.linalg.norm(Phi1_c1 - Phi1_c0, axis=1, keepdims=True)
50
+ norm_c2c1 = np.linalg.norm(Phi1_c2 - Phi1_c1, axis=1, keepdims=True)
51
+ norm_c0c2 = np.linalg.norm(Phi1_c0 - Phi1_c2, axis=1, keepdims=True)
52
+ l_max = np.max(np.hstack((norm_c1c0, norm_c2c1, norm_c0c2)), axis=1)
53
+ # compute Delta_min
54
+ tree = KDTree(Phi1)
55
+ dists = tree.query(Phi2 @ C)[0]
56
+ Delta_min = dists.flatten()
57
+ # compute delta_min
58
+ distmat1 = spatial.distance.cdist(Phi1_c0, Phi2 @ C)
59
+ distmat2 = spatial.distance.cdist(Phi1_c1, Phi2 @ C)
60
+ distmat3 = spatial.distance.cdist(Phi1_c2, Phi2 @ C)
61
+ delta_min_all = np.min((distmat1, distmat2, distmat3), axis=0)
62
+ # compute barycentric coordinates
63
+ face_match = np.zeros(n2, dtype=int)
64
+ bary_coord = np.zeros((n2, 3))
65
+ for vertind in range(n2):
66
+ query_faceinds = np.where(delta_min_all[:, vertind] - l_max < Delta_min[vertind])[0] # (npfi,)
67
+ query_triangles = Phi1[s1.f[query_faceinds], :] # (npfi, 3)
68
+ query_point = Phi2[vertind, :] @ C # (npfi, 3, k2)
69
+ dists, proj, bary_coords = project_p2t(query_triangles, query_point, return_bary=True)
70
+ min_ind = dists.argmin()
71
+ face_match[vertind] = query_faceinds[min_ind]
72
+ bary_coord[vertind] = bary_coords[min_ind]
73
+ # build precise map
74
+ v0 = s1.f[face_match, 0]
75
+ v1 = s1.f[face_match, 1]
76
+ v2 = s1.f[face_match, 2]
77
+ ii = np.arange(n2)
78
+ In = np.concatenate([ii, ii, ii])
79
+ Jn = np.concatenate([v0, v1, v2])
80
+ Sn = np.concatenate([bary_coord[:, 0], bary_coord[:, 1], bary_coord[:, 2]])
81
+ return sparse.csr_matrix((Sn, (In, Jn)), shape=(n2, n1))
@@ -0,0 +1,61 @@
1
+ ################################################################################
2
+ # #
3
+ # This file is part of the Morphomatics library #
4
+ # see https://github.com/morphomatics/morphomatics #
5
+ # #
6
+ # Copyright (C) 2025 Zuse Institute Berlin #
7
+ # #
8
+ # Morphomatics is distributed under the terms of the MIT License. #
9
+ # see $MORPHOMATICS/LICENSE #
10
+ # #
11
+ ################################################################################
12
+
13
+ import numpy as np
14
+ from scipy import sparse
15
+ from scipy.sparse import linalg
16
+ from ..geom import Surface
17
+
18
+ try:
19
+ from robust_laplacian import mesh_laplacian
20
+ except ImportError:
21
+ def mesh_laplacian(v, f):
22
+ s = Surface(v,f)
23
+ M = sparse.diags(s.vertex_areas_barycentric)
24
+ W = s.div @ s.grad
25
+ return W, M
26
+
27
+
28
+ class LaplaceBeltrami(object):
29
+
30
+ def __init__(self, surf):
31
+ self.surf = surf
32
+ self.mass = None
33
+ self.n_eig = None
34
+ self.evals = None
35
+ self.evecs = None
36
+
37
+ @property
38
+ def evecs_inv(self):
39
+ return self.evecs.T @ self.mass
40
+
41
+ def mass_fe(self):
42
+ n = len(self.surf.v)
43
+ faces = self.surf.f
44
+ face_areas = self.surf.face_areas
45
+ Me1 = sparse.csr_matrix((face_areas / 12, (faces[:, 1], faces[:, 2])), (n, n))
46
+ Me2 = sparse.csr_matrix((face_areas / 12, (faces[:, 2], faces[:, 0])), (n, n))
47
+ Me3 = sparse.csr_matrix((face_areas / 12, (faces[:, 0], faces[:, 1])), (n, n))
48
+ ind = np.hstack(faces.T)
49
+ Mii = sparse.csr_matrix((np.concatenate((face_areas, face_areas, face_areas)) / 6, (ind, ind)), (n, n))
50
+ self.mass = Me1 + Me1.T + Me2 + Me2.T + Me3 + Me3.T + Mii
51
+
52
+ def eig(self):
53
+ W, M = mesh_laplacian(np.asarray(self.surf.v), np.asarray(self.surf.f))
54
+ if self.mass:
55
+ self.evals, self.evecs = sparse.linalg.eigsh(W, self.n_eig, self.mass, sigma=-1e-8)
56
+ else:
57
+ self.mass = M
58
+ # solve S * v = lambda * M * v with change of variables u = M^.5 * v
59
+ sqrtMinv = sparse.diags(1 / np.sqrt(M.data))
60
+ self.evals, evecs = sparse.linalg.eigsh(sqrtMinv @ W @ sqrtMinv, self.n_eig, sigma=-1e-8)
61
+ self.evecs = sqrtMinv @ evecs
@@ -0,0 +1,88 @@
1
+ ################################################################################
2
+ # #
3
+ # This file is part of the Morphomatics library #
4
+ # see https://github.com/morphomatics/morphomatics #
5
+ # #
6
+ # Copyright (C) 2025 Zuse Institute Berlin #
7
+ # #
8
+ # Morphomatics is distributed under the terms of the MIT License. #
9
+ # see $MORPHOMATICS/LICENSE #
10
+ # #
11
+ ################################################################################
12
+
13
+ from tqdm import tqdm
14
+ from .convert import *
15
+
16
+
17
+ def icp(surfs, bases, C12_init, n_iter):
18
+ s1, s2 = surfs
19
+ Phi1, Phi2 = bases
20
+ M2 = sparse.diags(s2.vertex_areas_barycentric)
21
+ Phi2_pseudoinv = Phi2.T @ M2
22
+ n1, k1 = Phi1.shape
23
+ n2, k2 = Phi2.shape
24
+ C12 = C12_init
25
+ inds = None
26
+ for i in range(n_iter):
27
+ nn = NearestNeighbors(n_neighbors=1, n_jobs=-1).fit(Phi1[:, :k1])
28
+ inds = nn.kneighbors(Phi2[:, :k2] @ C12, return_distance=False)
29
+ inds = inds.squeeze()
30
+ P12 = sparse.csr_matrix((np.ones(n2), (np.linspace(0, n2 - 1, n2), inds)), shape=(n2, n1))
31
+ U, _, VT = np.linalg.svd(Phi2_pseudoinv @ P12 @ Phi1)
32
+ C12 = U @ sparse.eye(k2, k1) @ VT
33
+ return C12, inds
34
+
35
+
36
+ def zoomout(ops, C, n_steps, steps, convert=False):
37
+ k_t, k_s = C.shape
38
+ step_s, step_t = steps
39
+ for i in range(n_steps):
40
+ if convert == 'iso':
41
+ P = to_hat_iso([ops[0].evecs[:, :k_s], ops[1].evecs[:, :k_t]], C)
42
+ elif convert == 'precise':
43
+ P = to_precise(ops, C)
44
+ else:
45
+ P = to_hat([ops[0].evecs[:, :k_s], ops[1].evecs[:, :k_t]], C)
46
+ k_s, k_t = k_s + step_s, k_t + step_t
47
+ C = ops[1].evecs_inv[:k_t, :] @ P @ ops[0].evecs[:, :k_s]
48
+ return C
49
+
50
+
51
+ def zoomout_consistent(ops, adj, C, steps, p_lat, tol_lat=-1e-6):
52
+ n = len(ops)
53
+ n_edges = len(C)
54
+ k = len(C[0])
55
+ for step in tqdm(steps):
56
+ Id = np.tile(np.eye(k), (n_edges, 1, 1))
57
+ weights = adj.data[:, None, None]
58
+ n_lat = int(p_lat * k)
59
+ W = sparse.bsr_matrix((weights * C, adj.row, np.arange(n_edges + 1)))\
60
+ - sparse.bsr_matrix((weights * Id, adj.col, np.arange(n_edges + 1)))
61
+ W = W.T @ W
62
+ eigs, Y = sparse.linalg.eigsh(W, k=n_lat, sigma=tol_lat)
63
+ Y = Y.reshape((adj.shape[0], k, n_lat))
64
+ E = np.einsum('hji,hj,hjk', Y, [ops[i].evals[:k] for i in range(n)], Y)
65
+ Lambda_0, U = np.linalg.eigh(E)
66
+ Y = np.einsum('...ij,jk', Y, U)
67
+ k_new = k + step
68
+ for e, (i, j) in enumerate(zip(adj.row, adj.col)):
69
+ nn = NearestNeighbors(n_neighbors=1, n_jobs=-1).fit(ops[i].evecs[:, :k] @ Y[i])
70
+ inds_ij = nn.kneighbors(ops[j].evecs[:, :k] @ Y[j], return_distance=False)
71
+ inds_ij = inds_ij.squeeze()
72
+ ni, nj = len(ops[i].evecs), len(ops[j].evecs)
73
+ Pij = sparse.csr_matrix((np.ones(nj), (np.arange(nj), inds_ij)), shape=(nj, ni))
74
+ Cij = ops[j].evecs_inv[:k_new, :] @ Pij @ ops[i].evecs[:, :k_new]
75
+ C[e] = Cij
76
+ k = k_new
77
+ Id = np.tile(np.eye(k), (n_edges, 1, 1))
78
+ weights = adj.data[:, None, None]
79
+ n_lat = int(p_lat * k)
80
+ W = sparse.bsr_matrix((weights * C, adj.row, np.arange(n_edges + 1))) \
81
+ - sparse.bsr_matrix((weights * Id, adj.col, np.arange(n_edges + 1)))
82
+ W = W.T @ W
83
+ eigs, Y = sparse.linalg.eigsh(W, k=n_lat, sigma=tol_lat)
84
+ Y = Y.reshape((adj.shape[0], k, n_lat))
85
+ E = np.einsum('hji,hj,hjk', Y, [ops[i].evals[:k] for i in range(n)], Y)
86
+ Lambda_0, U = np.linalg.eigh(E)
87
+ Y = np.einsum('...ij,jk', Y, U)
88
+ return Lambda_0, Y, C
@@ -0,0 +1,356 @@
1
+ ################################################################################
2
+ # #
3
+ # This file is part of the Morphomatics library #
4
+ # see https://github.com/morphomatics/morphomatics #
5
+ # #
6
+ # Copyright (C) 2025 Zuse Institute Berlin #
7
+ # #
8
+ # Morphomatics is distributed under the terms of the MIT License. #
9
+ # see $MORPHOMATICS/LICENSE #
10
+ # #
11
+ ################################################################################
12
+
13
+ import numpy as np
14
+
15
+
16
+ def project_p2t(triangles, point, return_bary=False):
17
+ """
18
+ This functions projects a p-dimensional point on each of the given p-dimensional triangle.
19
+ All operations are parallelized, which makes the code quite hard to read. For an easier take,
20
+ follow the code in the function below (not written by me) for projection on a single triangle.
21
+
22
+ The first estimates for each triangle in which of the following region the point lies, then
23
+ solves for each region.
24
+
25
+
26
+ ^t
27
+ \ |
28
+ \reg2|
29
+ \ |
30
+ \ |
31
+ \ |
32
+ \|
33
+ *P2
34
+ |\
35
+ | \
36
+ reg3 | \ reg1
37
+ | \
38
+ |reg0\
39
+ | \
40
+ | \ P1
41
+ -------*-------*------->s
42
+ |P0 \
43
+ reg4 | reg5 \ reg6
44
+
45
+ Most notations come from :
46
+ [1] "David Eberly, 'Distance Between Point and Triangle in 3D',
47
+ Geometric Tools, LLC, (1999)"
48
+
49
+ parameters
50
+ -------------------------------
51
+ triangles : (m,3,p) set of m p-dimensional triangles
52
+ point : (p,) coordinates of the point
53
+ return_bary : Whether to return barycentric coordinates inside each triangle
54
+
55
+ returns
56
+ -------------------------------
57
+ final_dists : (m,) distance from the point to each of the triangle
58
+ projections : (m,p) coordinates of the projected point
59
+ bary_coords : (m,3) barycentric coordinates of the projection within each triangle
60
+ """
61
+
62
+ if point.ndim == 2:
63
+ point = point.squeeze() # (1,p)
64
+
65
+ # rewrite triangles in normal form base + axis
66
+ bases = triangles[:, 0] # (m,p)
67
+ axis1 = triangles[:, 1] - bases # (m,p)
68
+ axis2 = triangles[:, 2] - bases # (m,p)
69
+
70
+ diff = bases - point[None, :] # (m,p)
71
+
72
+ # Precompute quantities with notations from [1]
73
+
74
+ a = np.einsum('ij,ij->i', axis1, axis1) # (m,)
75
+ b = np.einsum('ij,ij->i', axis1, axis2) # (m,)
76
+ c = np.einsum('ij,ij->i', axis2, axis2) # (m,)
77
+ d = np.einsum('ij,ij->i', axis1, diff) # (m,)
78
+ e = np.einsum('ij,ij->i', axis2, diff) # (m,)
79
+ f = np.einsum('ij,ij->i', diff, diff) # (m,)
80
+
81
+ det = a * c - b ** 2 # (m,)
82
+ s = b * e - c * d # (m,)
83
+ t = b * d - a * e # (m,)
84
+
85
+ # Array of barycentric coordinates (s,t) and distances
86
+ final_s = np.zeros(s.size) # (m,)
87
+ final_t = np.zeros(t.size) # (m,)
88
+ final_dists = np.zeros(t.size) # (m,)
89
+
90
+ # Find for which triangles which zone the point belongs to
91
+
92
+ # s + t <= det
93
+ test1 = (s + t <= det) # (m,) with (m1) True values
94
+ inds_0345 = np.where(test1)[0] # (m1)
95
+ inds_126 = np.where(~test1)[0] # (m-m1)
96
+
97
+ # s < 0 | s + t <= det
98
+ test11 = s[inds_0345] < 0 # (m1,) with (m11) True values
99
+ inds_34 = inds_0345[test11] # (m11)
100
+ inds_05 = inds_0345[~test11] # (m1-m11)
101
+
102
+ # t < 0 | (s + t <= det) and (s < 0)
103
+ test111 = t[inds_34] < 0 # (m11) with (m111) True values
104
+ inds_4 = inds_34[test111] # (m111)
105
+ inds_3 = inds_34[~test111] # (m11 - m111)
106
+
107
+ # t < 0 | s + t <= det and (s >= 0)
108
+ test12 = t[inds_05] < 0 # (m-m11) with (m12) True values
109
+ inds_5 = inds_05[test12] # (m12;)
110
+ inds_0 = inds_05[~test12] # (m-m11-m12,)
111
+
112
+ # s < 0 | s + t > det
113
+ test21 = s[inds_126] < 0 # (m-m1) with (m21) True values
114
+ inds_2 = inds_126[test21] # (m21,)
115
+ inds_16 = inds_126[~test21] # (m-m1-m21)
116
+
117
+ # t < 0 | (s + t > det) and (s > 0)
118
+ test22 = t[inds_16] < 0 # (m-m1-m21) with (m22) True values
119
+ inds_6 = inds_16[test22] # (m22,)
120
+ inds_1 = inds_16[~test22] # (m-m1-m21-m22)
121
+
122
+ # DEAL REGION BY REGION (in parallel within each)
123
+
124
+ # REGION 4
125
+ if len(inds_4) > 0:
126
+ # print('Case 4',inds_4)
127
+ test4_1 = d[inds_4] < 0
128
+ inds4_1 = inds_4[test4_1]
129
+ inds4_2 = inds_4[~test4_1]
130
+
131
+ # FIRST PART - SUBDIVIDE IN 2
132
+ final_t[inds4_1] = 0 # Useless already done
133
+
134
+ test4_11 = (-d[inds4_1] >= a[inds4_1])
135
+ inds4_11 = inds4_1[test4_11]
136
+ inds4_12 = inds4_1[~test4_11]
137
+
138
+ final_s[inds4_11] = 1.
139
+ final_dists[inds4_11] = a[inds4_11] + 2.0 * d[inds4_11] + f[inds4_11]
140
+
141
+ final_s[inds4_12] = -d[inds4_12] / a[inds4_12]
142
+ final_dists[inds4_12] = d[inds4_12] * s[inds4_12] + f[inds4_12]
143
+
144
+ # SECOND PART - SUBDIVIDE IN 2
145
+ final_s[inds4_2] = 0 # Useless already done
146
+
147
+ test4_21 = (e[inds4_2] >= 0)
148
+ inds4_21 = inds4_2[test4_21]
149
+ inds4_22 = inds4_2[~test4_21]
150
+
151
+ final_t[inds4_21] = 0
152
+ final_dists[inds4_21] = f[inds4_21]
153
+
154
+ # SECOND PART OF SECOND PART - SUBDIVIDE IN 2
155
+ test4_221 = (-e[inds4_22] >= c[inds4_22])
156
+ inds4_221 = inds4_22[test4_221]
157
+ inds4_222 = inds4_22[~test4_221]
158
+
159
+ final_t[inds4_221] = 1
160
+ final_dists[inds4_221] = c[inds4_221] + 2.0 * e[inds4_221] + f[inds4_221]
161
+
162
+ final_t[inds4_222] = -e[inds4_222] / c[inds4_222]
163
+ final_dists[inds4_222] = e[inds4_222] * t[inds4_222] + f[inds4_222]
164
+
165
+ if len(inds_3) > 0:
166
+ # print('Case 3', inds_3)
167
+ final_s[inds_3] = 0
168
+
169
+ test3_1 = e[inds_3] >= 0
170
+ inds3_1 = inds_3[test3_1]
171
+ inds3_2 = inds_3[~test3_1]
172
+
173
+ final_t[inds3_1] = 0
174
+ final_dists[inds3_1] = f[inds3_1]
175
+
176
+ # SECOND PART - SUBDIVIDE IN 2
177
+
178
+ test3_21 = (-e[inds3_2] >= c[inds3_2])
179
+ inds3_21 = inds3_2[test3_21]
180
+ inds3_22 = inds3_2[~test3_21]
181
+
182
+ # print(inds3_21, inds3_22)
183
+
184
+ final_t[inds3_21] = 1
185
+ final_dists[inds3_21] = c[inds3_21] + 2.0 * e[inds3_21] + f[inds3_21]
186
+
187
+ final_t[inds3_22] = -e[inds3_22] / c[inds3_22]
188
+ final_dists[inds3_22] = e[inds3_22] * final_t[inds3_22] + f[inds3_22] # -e*t ????
189
+
190
+ if len(inds_5) > 0:
191
+ # print('Case 5', inds_5)
192
+ final_t[inds_5] = 0
193
+
194
+ test5_1 = d[inds_5] >= 0
195
+ inds5_1 = inds_5[test5_1]
196
+ inds5_2 = inds_5[~test5_1]
197
+
198
+ final_s[inds5_1] = 0
199
+ final_dists[inds5_1] = f[inds5_1]
200
+
201
+ test5_21 = (-d[inds5_2] >= a[inds5_2])
202
+ inds5_21 = inds5_2[test5_21]
203
+ inds5_22 = inds5_2[~test5_21]
204
+
205
+ final_s[inds5_21] = 1
206
+ final_dists[inds5_21] = a[inds5_21] + 2.0 * d[inds5_21] + f[inds5_21]
207
+
208
+ final_s[inds5_22] = -d[inds5_22] / a[inds5_22]
209
+ final_dists[inds5_22] = d[inds5_22] * final_s[inds5_22] + f[inds5_22]
210
+
211
+ if len(inds_0) > 0:
212
+ # print('Case 0', inds_0)
213
+ invDet = 1.0 / det[inds_0]
214
+ final_s[inds_0] = s[inds_0] * invDet
215
+ final_t[inds_0] = t[inds_0] * invDet
216
+ final_dists[inds_0] = final_s[inds_0] * (
217
+ a[inds_0] * final_s[inds_0] + b[inds_0] * final_t[inds_0] + 2.0 * d[inds_0]) + \
218
+ final_t[inds_0] * (
219
+ b[inds_0] * final_s[inds_0] + c[inds_0] * final_t[inds_0] + 2.0 * e[inds_0]) + \
220
+ f[inds_0]
221
+
222
+ if len(inds_2) > 0:
223
+ # print('Case 2', inds_2)
224
+
225
+ tmp0 = b[inds_2] + d[inds_2]
226
+ tmp1 = c[inds_2] + e[inds_2]
227
+
228
+ test2_1 = tmp1 > tmp0
229
+ inds2_1 = inds_2[test2_1]
230
+ inds2_2 = inds_2[~test2_1]
231
+
232
+ numer = tmp1[test2_1] - tmp0[test2_1]
233
+ denom = a[inds2_1] - 2.0 * b[inds2_1] + c[inds2_1]
234
+
235
+ test2_11 = (numer >= denom)
236
+ inds2_11 = inds2_1[test2_11]
237
+ inds2_12 = inds2_1[~test2_11]
238
+
239
+ final_s[inds2_11] = 1
240
+ final_t[inds2_11] = 0
241
+ final_dists[inds2_11] = a[inds2_11] + 2.0 * d[inds2_11] + f[inds2_11]
242
+
243
+ final_s[inds2_12] = numer[~test2_11] / denom[~test2_11]
244
+ final_t[inds2_12] = 1 - final_s[inds2_12]
245
+ final_dists[inds2_12] = final_s[inds2_12] * (
246
+ a[inds2_12] * final_s[inds2_12] + b[inds2_12] * final_t[inds2_12] + 2 * d[inds2_12]) + \
247
+ final_t[inds2_12] * (
248
+ b[inds2_12] * final_s[inds2_12] + c[inds2_12] * final_t[inds2_12] + 2 * e[
249
+ inds2_12]) + f[inds2_12]
250
+
251
+ final_s[inds2_2] = 0.
252
+
253
+ test2_21 = (tmp1[~test2_1] <= 0.)
254
+ inds2_21 = inds2_2[test2_21]
255
+ inds2_22 = inds2_2[~test2_21]
256
+
257
+ final_t[inds2_21] = 1
258
+ final_dists[inds2_21] = c[inds2_21] + 2.0 * e[inds2_21] + f[inds2_21]
259
+
260
+ test2_221 = (e[inds2_22] >= 0.)
261
+ inds2_221 = inds2_22[test2_221]
262
+ inds2_222 = inds2_22[~test2_221]
263
+
264
+ final_t[inds2_221] = 0.
265
+ final_dists[inds2_221] = f[inds2_221]
266
+
267
+ final_t[inds2_222] = -e[inds2_222] / c[inds2_222]
268
+ final_dists[inds2_222] = e[inds2_222] * final_t[inds2_222] + f[inds2_222]
269
+
270
+ if len(inds_6) > 0:
271
+ # print('Case 6', inds_6)
272
+ tmp0 = b[inds_6] + e[inds_6]
273
+ tmp1 = a[inds_6] + d[inds_6]
274
+
275
+ test6_1 = tmp1 > tmp0
276
+ inds6_1 = inds_6[test6_1]
277
+ inds6_2 = inds_6[~test6_1]
278
+
279
+ numer = tmp1[test6_1] - tmp0[test6_1]
280
+ denom = a[inds6_1] - 2.0 * b[inds6_1] + c[inds6_1]
281
+
282
+ test6_11 = (numer >= denom)
283
+ inds6_11 = inds6_1[test6_11]
284
+ inds6_12 = inds6_1[~test6_11]
285
+
286
+ final_t[inds6_11] = 1
287
+ final_s[inds6_11] = 0
288
+ final_dists[inds6_11] = c[inds6_11] + 2.0 * e[inds6_11] + f[inds6_11]
289
+
290
+ final_t[inds6_12] = numer[~test6_11] / denom[~test6_11]
291
+ final_s[inds6_12] = 1 - final_t[inds6_12]
292
+ final_dists[inds6_12] = final_s[inds6_12] * (a[inds6_12] * final_s[inds6_12] +
293
+ b[inds6_12] * final_t[inds6_12] + 2.0 * d[inds6_12]) + \
294
+ final_t[inds6_12] * (b[inds6_12] * final_s[inds6_12] +
295
+ c[inds6_12] * final_t[inds6_12] + 2.0 * e[inds6_12]) + f[inds6_12]
296
+
297
+ final_t[inds6_2] = 0.
298
+
299
+ test6_21 = (tmp1[~test6_1] <= 0.)
300
+ inds6_21 = inds6_2[test6_21]
301
+ inds6_22 = inds6_2[~test6_21]
302
+
303
+ final_s[inds6_21] = 1
304
+ final_dists[inds6_21] = a[inds6_21] + 2.0 * d[inds6_21] + f[inds6_21]
305
+
306
+ test6_221 = (d[inds6_22] >= 0.)
307
+ inds6_221 = inds6_22[test6_221]
308
+ inds6_222 = inds6_22[~test6_221]
309
+
310
+ final_s[inds6_221] = 0.
311
+ final_dists[inds6_221] = f[inds6_221]
312
+
313
+ final_s[inds6_222] = -d[inds6_222] / a[inds6_222]
314
+ final_dists[inds6_222] = d[inds6_222] * final_s[inds6_222] + f[inds6_222]
315
+
316
+ if len(inds_1) > 0:
317
+ # print('Case 1', inds_1)
318
+ numer = c[inds_1] + e[inds_1] - b[inds_1] - d[inds_1]
319
+
320
+ test1_1 = numer <= 0
321
+ inds1_1 = inds_1[test1_1]
322
+ inds1_2 = inds_1[~test1_1]
323
+
324
+ final_s[inds1_1] = 0
325
+ final_t[inds1_1] = 1
326
+ final_dists[inds1_1] = c[inds1_1] + 2.0 * e[inds1_1] + f[inds1_1]
327
+
328
+ denom = a[inds1_2] - 2.0 * b[inds1_2] + c[inds1_2]
329
+
330
+ test1_21 = (numer[~test1_1] >= denom)
331
+ # print(denom, numer, numer[~test1_1], test1_21, inds1_2)
332
+ inds1_21 = inds1_2[test1_21]
333
+ inds1_22 = inds1_2[~test1_21]
334
+
335
+ final_s[inds1_21] = 1
336
+ final_t[inds1_21] = 0
337
+ final_dists[inds1_21] = a[inds1_21] + 2.0 * d[inds1_21] + f[inds1_21]
338
+
339
+ final_s[inds1_22] = numer[~test1_1][~test1_21] / denom[~test1_21]
340
+ final_t[inds1_22] = 1 - final_s[inds1_22]
341
+ final_dists[inds1_22] = final_s[inds1_22] * (
342
+ a[inds1_22] * final_s[inds1_22] + b[inds1_22] * final_t[inds1_22] + 2.0 * d[inds1_22]) + \
343
+ final_t[inds1_22] * (
344
+ b[inds1_22] * final_s[inds1_22] + c[inds1_22] * final_t[inds1_22] + 2.0 * e[
345
+ inds1_22]) + f[inds1_22]
346
+
347
+ final_dists[final_dists < 0] = 0
348
+ final_dists = np.sqrt(final_dists)
349
+
350
+ projections = bases + final_s[:, None] * axis1 + final_t[:, None] * axis2
351
+ if return_bary:
352
+ bary_coords = np.concatenate([1 - final_s[:, None] - final_t[:, None], final_s[:, None], final_t[:, None]],
353
+ axis=1)
354
+ return final_dists, projections, bary_coords
355
+
356
+ return final_dists, projections
@@ -3,7 +3,7 @@
3
3
  # This file is part of the Morphomatics library #
4
4
  # see https://github.com/morphomatics/morphomatics #
5
5
  # #
6
- # Copyright (C) 2024 Zuse Institute Berlin #
6
+ # Copyright (C) 2025 Zuse Institute Berlin #
7
7
  # #
8
8
  # Morphomatics is distributed under the terms of the MIT License. #
9
9
  # see $MORPHOMATICS/LICENSE #
@@ -3,7 +3,7 @@
3
3
  # This file is part of the Morphomatics library #
4
4
  # see https://github.com/morphomatics/morphomatics #
5
5
  # #
6
- # Copyright (C) 2024 Zuse Institute Berlin #
6
+ # Copyright (C) 2025 Zuse Institute Berlin #
7
7
  # #
8
8
  # Morphomatics is distributed under the terms of the MIT License. #
9
9
  # see $MORPHOMATICS/LICENSE #