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.
- {morphomatics-4.0 → morphomatics-4.1}/PKG-INFO +14 -2
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/__init__.py +1 -1
- morphomatics-4.1/morphomatics/correspondence/__init__.py +15 -0
- morphomatics-4.1/morphomatics/correspondence/convert.py +81 -0
- morphomatics-4.1/morphomatics/correspondence/laplacian.py +61 -0
- morphomatics-4.1/morphomatics/correspondence/refine.py +88 -0
- morphomatics-4.1/morphomatics/correspondence/util.py +356 -0
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/geom/__init__.py +1 -1
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/geom/bezier_spline.py +1 -1
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/geom/misc.py +5 -2
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/geom/surface.py +28 -14
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/graph/__init__.py +1 -1
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/graph/operators.py +1 -1
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/__init__.py +4 -1
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/bezierfold.py +9 -16
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/connection.py +1 -1
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/cubic_bezierfold.py +1 -8
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/differential_coords.py +1 -34
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/discrete_ops.py +1 -1
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/euclidean.py +4 -31
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/fundamental_coords.py +1 -34
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/gl_p_coords.py +3 -6
- morphomatics-4.1/morphomatics/manifold/gl_p_n.py +196 -0
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/grassmann.py +16 -15
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/hyperbolic_space.py +1 -8
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/kendall.py +5 -17
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/lie_group.py +27 -20
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/manifold.py +1 -36
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/manopt_wrapper.py +1 -1
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/metric.py +34 -8
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/point_distribution_model.py +3 -12
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/power_manifold.py +60 -61
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/product_manifold.py +25 -35
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/se_3.py +63 -139
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/shape_space.py +1 -1
- morphomatics-4.1/morphomatics/manifold/simplex.py +207 -0
- morphomatics-4.1/morphomatics/manifold/size_and_shape.py +199 -0
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/so_3.py +32 -95
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/spd.py +16 -57
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/sphere.py +6 -9
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/tangent_bundle.py +1 -8
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/manifold/util.py +62 -1
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/nn/__init__.py +2 -1
- morphomatics-4.1/morphomatics/nn/euclidean_layers.py +36 -0
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/nn/flow_layers.py +31 -19
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/nn/tangent_layers.py +1 -1
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/nn/wFM_layers.py +1 -1
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/opt/__init__.py +1 -1
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/opt/riemannian_newton_raphson.py +1 -1
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/opt/riemannian_steepest_descent.py +6 -5
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/stats/__init__.py +3 -2
- morphomatics-4.0/morphomatics/stats/biinvariant_statistics.py → morphomatics-4.1/morphomatics/stats/biinvariant_dissimilarity_measures.py +8 -5
- morphomatics-4.1/morphomatics/stats/biinvariant_regression.py +203 -0
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/stats/exponential_barycenter.py +1 -1
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/stats/geometric_median.py +1 -1
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/stats/principal_geodesic_analysis.py +1 -1
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/stats/riemannian_regression.py +1 -1
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/stats/statistical_shape_model.py +1 -1
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics.egg-info/PKG-INFO +14 -2
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics.egg-info/SOURCES.txt +10 -1
- {morphomatics-4.0 → morphomatics-4.1}/setup.py +3 -2
- morphomatics-4.0/morphomatics/manifold/gl_p_n.py +0 -201
- {morphomatics-4.0 → morphomatics-4.1}/LICENSE +0 -0
- {morphomatics-4.0 → morphomatics-4.1}/README.md +0 -0
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics/nn/train.py +0 -0
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics.egg-info/dependency_links.txt +0 -0
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics.egg-info/requires.txt +0 -0
- {morphomatics-4.0 → morphomatics-4.1}/morphomatics.egg-info/top_level.txt +0 -0
- {morphomatics-4.0 → morphomatics-4.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: morphomatics
|
|
3
|
-
Version: 4.
|
|
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"/>
|
|
@@ -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)
|
|
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)
|
|
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 #
|