Skip to content

Commit c69b7cd

Browse files
Started merging: test passes
1 parent 0f03419 commit c69b7cd

File tree

6 files changed

+133
-193
lines changed

6 files changed

+133
-193
lines changed

pygem/bffd.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,13 @@ class BFFD(CFFD):
1616
'''
1717
Class that handles the Barycenter Free Form Deformation on the mesh points.
1818
19-
:param list n_control_points: number of control points in the x, y, and z
20-
direction. Default is [2, 2, 2].
21-
2219
:param list n_control_points: number of control points in the x, y, and z
2320
direction. Default is [2, 2, 2].
2421
2522
:cvar numpy.ndarray box_length: dimension of the FFD bounding box, in the
2623
x, y and z direction (local coordinate system).
2724
:cvar numpy.ndarray box_origin: the x, y and z coordinates of the origin of
2825
the FFD bounding box.
29-
:cvar numpy.ndarray rot_angle: rotation angle around x, y and z axis of the
30-
FFD bounding box.
3126
:cvar numpy.ndarray n_control_points: the number of control points in the
3227
x, y, and z direction.
3328
:cvar numpy.ndarray array_mu_x: collects the displacements (weights) along
@@ -51,22 +46,28 @@ class BFFD(CFFD):
5146
5247
>>> from pygem import BFFD
5348
>>> import numpy as np
54-
>>> b=np.random.rand(3)
55-
>>> bffd = BFFD([2,2,2],b)
49+
>>> b = np.random.rand(3)
50+
>>> bffd = BFFD(b, [2, 2, 2])
5651
>>> bffd.read_parameters('tests/test_datasets/parameters_test_ffd_sphere.prm')
5752
>>> original_mesh_points = np.load('tests/test_datasets/meshpoints_sphere_orig.npy')
5853
>>> bffd.adjust_control_points(original_mesh_points[:-4])
59-
>>> assert np.isclose(np.linalg.norm(bffd.fun(bffd.ffd(original_mesh_points[:-4]))-b),np.array([0.]))
54+
>>> assert np.isclose(np.linalg.norm(bffd.fun(bffd.ffd(original_mesh_points[:-4])) - b), np.array([0.]))
6055
>>> new_mesh_points = bffd.ffd(original_mesh_points)
6156
'''
57+
6258
def __init__(self,
59+
fixval=None,
6360
n_control_points=None,
64-
fixval=None,
65-
weight_matrix=None,
66-
mask=None):
67-
super().__init__(n_control_points, None, fixval, weight_matrix, mask)
61+
ffd_mask=None):
62+
super().__init__(fixval,None,n_control_points,ffd_mask,None)
6863

6964
def linfun(x):
7065
return np.mean(x.reshape(-1, 3), axis=0)
7166

7267
self.fun = linfun
68+
self.fixval = fixval
69+
self.fun_mask = np.array([[True, False, False],
70+
[False, True, False],
71+
[False, False, True]])
72+
73+

pygem/cffd.py

Lines changed: 65 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,12 @@ class CFFD(FFD):
1919
2020
:param list n_control_points: number of control points in the x, y, and z
2121
direction. Default is [2, 2, 2].
22-
22+
:param string mode: it can be ``affine`` or ``triaffine``. The first option is for the F that are affine in all the coordinates of the points.
23+
The second one is for functions that are F in the coordinates of the points. The first option implies the second, but is optimal for that class of functions.
2324
:cvar numpy.ndarray box_length: dimension of the FFD bounding box, in the
2425
x, y and z direction (local coordinate system).
2526
:cvar numpy.ndarray box_origin: the x, y and z coordinates of the origin of
2627
the FFD bounding box.
27-
:cvar numpy.ndarray rot_angle: rotation angle around x, y and z axis of the
28-
FFD bounding box.
2928
:cvar numpy.ndarray n_control_points: the number of control points in the
3029
x, y, and z direction.
3130
:cvar numpy.ndarray array_mu_x: collects the displacements (weights) along
@@ -36,79 +35,100 @@ class CFFD(FFD):
3635
z, normalized with the box length z.
3736
:cvar callable fun: it defines the F of the constraint F(x)=c. Default is the constant 1 function.
3837
:cvar numpy.ndarray fixval: it defines the c of the constraint F(x)=c. Default is 1.
39-
:cvar numpy.ndarray mask: a boolean tensor that tells to the class
38+
:cvar numpy.ndarray ffd_mask: a boolean tensor that tells to the class
4039
which control points can be moved, and in what direction, to enforce the constraint.
4140
The tensor has shape (n_x,n_y,n_z,3), where the last dimension indicates movement
4241
on x,y,z respectively. Default is all true.
43-
:cvar numpy.ndarray weight_matrix: a symmetric positive definite weigth matrix.
44-
It must be of row and column size the number of trues in the mask.
45-
It weights the movemement of the control points which have a true flag in the mask.
46-
Default is identity.
42+
:cvar numpy.ndarray fun_mask: a boolean tensor that tells to the class
43+
on which axis which constraint depends on. The tensor has shape (n_cons,3), where the last dimension indicates dependency on
44+
on x,y,z respectively. Default is all true. It used only in the triaffine mode.
45+
4746
4847
:Example:
4948
5049
>>> from pygem import CFFD
5150
>>> import numpy as np
5251
>>> original_mesh_points = np.load('tests/test_datasets/meshpoints_sphere_orig.npy')
53-
>>> A=np.random.rand(3,original_mesh_points[:-4].reshape(-1).shape[0])
54-
>>> fun=lambda x: A@x.reshape(-1)
55-
>>> b=np.random.rand(3)
56-
>>> cffd = CFFD([2,2,2],fun,b)
52+
>>> A = np.random.rand(3, original_mesh_points[:-4].reshape(-1).shape[0])
53+
>>> fun = lambda x: A @ x.reshape(-1)
54+
>>> b = np.random.rand(3)
55+
>>> cffd = CFFD(b, fun, [2, 2, 2])
5756
>>> cffd.read_parameters('tests/test_datasets/parameters_test_ffd_sphere.prm')
5857
>>> cffd.adjust_control_points(original_mesh_points[:-4])
59-
>>> assert np.isclose(np.linalg.norm(fun(cffd.ffd(original_mesh_points[:-4]))-b),np.array([0.]),atol=1e-06)
58+
>>> assert np.isclose(np.linalg.norm(fun(cffd.ffd(original_mesh_points[:-4])) - b), np.array([0.]),atol = 1e-06)
6059
>>> new_mesh_points = cffd.ffd(original_mesh_points)
6160
"""
61+
6262
def __init__(self,
63-
n_control_points=None,
64-
fun=None,
65-
fixval=None,
66-
weight_matrix=None,
67-
mask=None):
63+
fixval,
64+
fun,
65+
n_control_points=None,
66+
ffd_mask=None,
67+
fun_mask=None):
6868
super().__init__(n_control_points)
6969

70-
if mask is None:
71-
self.mask = np.full((*self.n_control_points, 3), True, dtype=bool)
70+
if ffd_mask is None:
71+
self.ffd_mask = np.full((*self.n_control_points, 3), True, dtype=bool)
7272
else:
73-
self.mask = mask
73+
self.ffd_mask = ffd_mask
7474

75-
if fixval is None:
76-
self.fixval = np.array([1.])
75+
self.num_cons=len(fixval)
76+
self.fun=fun
77+
self.fixval=fixval
78+
if fun_mask is None:
79+
self.fun_mask = np.full((self.num_cons, 3), True, dtype=bool)
7780
else:
78-
self.fixval = fixval
79-
80-
if fun is None:
81-
self.fun = lambda x: self.fixval
82-
83-
else:
84-
self.fun = fun
85-
86-
if weight_matrix is None:
87-
self.weight_matrix = np.eye(np.sum(self.mask.astype(int)))
88-
89-
def adjust_control_points(self, src_pts):
81+
self.fun_mask = fun_mask
82+
def _adjust_control_points_inner(self, src_pts,i):
9083
'''
91-
Adjust the FFD control points such that fun(ffd(src_pts))=fixval
92-
84+
Solves the constrained optimization problem of axis i
85+
9386
:param np.ndarray src_pts: the points whose deformation we want to be
9487
constrained.
88+
:param int i: the axis we are considering.
9589
:rtype: None.
9690
'''
9791

9892
saved_parameters = self._save_parameters()
9993
indices = np.arange(np.prod(self.n_control_points) *
100-
3)[self.mask.reshape(-1)]
94+
3)[self.ffd_mask.reshape(-1)]
10195
A, b = self._compute_linear_map(src_pts, saved_parameters.copy(),
102-
indices)
96+
indices)
97+
A=A[self.fun_mask[:,i].reshape(-1),:]
98+
b=b[self.fun_mask[:,i].reshape(-1)]
10399
d = A @ saved_parameters[indices] + b
104-
invM = np.linalg.inv(self.weight_matrix)
100+
fixval=self.fixval[self.fun_mask[:,i].reshape(-1)]
105101
deltax = np.linalg.multi_dot([
106-
invM, A.T,
107-
np.linalg.inv(np.linalg.multi_dot([A, invM, A.T])),
108-
(self.fixval - d)
102+
A.T,
103+
np.linalg.inv(np.linalg.multi_dot([A, A.T])),
104+
(fixval - d)
109105
])
110106
saved_parameters[indices] = saved_parameters[indices] + deltax
111107
self._load_parameters(saved_parameters)
108+
return np.linalg.norm(deltax.reshape(-1))
109+
110+
111+
112+
def adjust_control_points(self,src_pts):
113+
'''
114+
Adjust the FFD control points such that fun(ffd(src_pts))=fixval
115+
116+
:param np.ndarray src_pts: the points whose deformation we want to be
117+
constrained.
118+
:rtype: None.
119+
'''
120+
vweight=self.fun_mask.copy().astype(float)
121+
vweight=vweight/np.sum(vweight,axis=1)
122+
mask_bak = self.ffd_mask.copy()
123+
diffvolume = self.fixval - self.fun(self.ffd(src_pts))
124+
for i in range(3):
125+
self.ffd_mask = np.full((*self.n_control_points, 3), False, dtype=bool)
126+
self.ffd_mask[:, :, :, i] = mask_bak[:, :, :, i].copy()
127+
self.fixval = self.fun(self.ffd(src_pts)) + vweight[:,i] * (
128+
diffvolume
129+
)
130+
self._adjust_control_points_inner(src_pts,i)
131+
112132

113133
def ffd(self, src_pts):
114134
'''
@@ -145,14 +165,6 @@ def _load_parameters(self, tmp):
145165
self.array_mu_y = tmp[:, :, :, 1]
146166
self.array_mu_z = tmp[:, :, :, 2]
147167

148-
def read_parameters(self, filename='parameters.prm'):
149-
super().read_parameters(filename)
150-
self.mask = np.full((*self.n_control_points, 3), True, dtype=bool)
151-
self.weight_matrix = np.eye(np.sum(self.mask.astype(int)))
152-
153-
154-
# I see that a similar function already exists in pygem.utils, but it does not work for inputs and outputs of different dimensions
155-
156168
def _compute_linear_map(self, src_pts, saved_parameters, indices):
157169
'''
158170
Computes the coefficient and the intercept of the linear map from the control points to the output.
@@ -184,3 +196,5 @@ def _compute_linear_map(self, src_pts, saved_parameters, indices):
184196
A = sol[0].T[:, :-1] #coefficient
185197
b = sol[0].T[:, -1] #intercept
186198
return A, b
199+
200+

pygem/vffd.py

Lines changed: 23 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,11 @@ class VFFD(CFFD):
1818
Class that handles the Volumetric Free Form Deformation on the mesh points.
1919
2020
:param list n_control_points: number of control points in the x, y, and z
21-
direction. Default is [2, 2, 2].
22-
23-
:param list n_control_points: number of control points in the x, y, and z
24-
direction. Default is [2, 2, 2].
25-
21+
direction. Default is [2, 2, 2].
2622
:cvar numpy.ndarray box_length: dimension of the FFD bounding box, in the
2723
x, y and z direction (local coordinate system).
2824
:cvar numpy.ndarray box_origin: the x, y and z coordinates of the origin of
2925
the FFD bounding box.
30-
:cvar numpy.ndarray rot_angle: rotation angle around x, y and z axis of the
31-
FFD bounding box.
3226
:cvar numpy.ndarray n_control_points: the number of control points in the
3327
x, y, and z direction.
3428
:cvar numpy.ndarray array_mu_x: collects the displacements (weights) along
@@ -39,57 +33,45 @@ class VFFD(CFFD):
3933
z, normalized with the box length z.
4034
:cvar callable fun: it defines the F of the constraint F(x)=c. Default is the constant 1 function.
4135
:cvar numpy.ndarray fixval: it defines the c of the constraint F(x)=c. Default is 1.
42-
:cvar numpy.ndarray mask: a boolean tensor that tells to the class
36+
:cvar numpy.ndarray ffd_mask: a boolean tensor that tells to the class
4337
which control points can be moved, and in what direction, to enforce the constraint.
4438
The tensor has shape (n_x,n_y,n_z,3), where the last dimension indicates movement
4539
on x,y,z respectively. Default is all true.
46-
:cvar numpy.ndarray weight_matrix: a symmetric positive definite weigth matrix.
47-
It must be of row and column size the number of trues in the mask.
48-
It weights the movemement of the control points which have a true flag in the mask.
49-
Default is identity.
50-
:cvar np.ndarray vweight: specifies the weight of every step of the volume enforcement.
5140
5241
:Example:
5342
5443
>>> from pygem import VFFD
5544
>>> import numpy as np
5645
>>> import meshio
5746
>>> mesh = meshio.read('tests/test_datasets/test_sphere.stl')
58-
>>> original_mesh_points=mesh.points
47+
>>> original_mesh_points = mesh.points
5948
>>> triangles = mesh.cells_dict["triangle"]
60-
>>> b=np.random.rand()
61-
>>> vffd = VFFD(triangles,[2,2,2],b)
49+
>>> b = np.random.rand()
50+
>>> vffd = VFFD(triangles, b,[2, 2, 2])
6251
>>> vffd.read_parameters('tests/test_datasets/parameters_test_ffd_sphere.prm')
6352
>>> vffd.adjust_control_points(original_mesh_points)
6453
>>> new_mesh_points = vffd(original_mesh_points)
65-
>>> assert np.isclose(np.linalg.norm(vffd.fun(new_mesh_points)-b),np.array([0.]),atol=1e-07)
54+
>>> assert np.isclose(np.linalg.norm(vffd.fun(new_mesh_points) - b),np.array([0.]), atol=1e-07)
6655
6756
'''
57+
6858
def __init__(self,
69-
triangles,
70-
n_control_points=None,
71-
fixval=None,
72-
weight_matrix=None,
73-
mask=None):
74-
super().__init__(n_control_points, None, fixval, weight_matrix, mask)
75-
self.triangles = triangles
76-
self.vweight = [1 / 3, 1 / 3, 1 / 3]
59+
triangles,
60+
fixval,
61+
n_control_points=None,
62+
ffd_mask=None):
63+
super().__init__(fixval,None,n_control_points,ffd_mask,None)
7764

78-
def volume(x):
79-
x = x.reshape(-1, 3)
80-
mesh = x[self.triangles]
81-
return np.sum(np.linalg.det(mesh))
65+
self.triangles=triangles
66+
def volume_inn(x):
67+
return _volume(x,self.triangles)
8268

83-
self.fun = volume
69+
self.fun = volume_inn
70+
self.fixval=fixval
71+
self.fun_mask=np.array([[True, True, True]])
72+
73+
def _volume(x,triangles):
74+
x = x.reshape(-1, 3)
75+
mesh = x[triangles]
76+
return np.array([np.sum(np.linalg.det(mesh))])
8477

85-
def adjust_control_points(self, src_pts):
86-
self.vweight = np.abs(self.vweight) / np.sum(np.abs(self.vweight))
87-
mask_bak = self.mask.copy()
88-
diffvolume = self.fixval - self.fun(self.ffd(src_pts))
89-
for i in range(3):
90-
self.mask = np.full((*self.n_control_points, 3), False, dtype=bool)
91-
self.mask[:, :, :, i] = mask_bak[:, :, :, i].copy()
92-
self.weight_matrix = np.eye(np.sum(self.mask.astype(int)))
93-
self.fixval = self.fun(
94-
self.ffd(src_pts)) + self.vweight[i] * (diffvolume) #in this way the constraint is enforced in three steps (one per every dim)
95-
super().adjust_control_points(src_pts)

tests/test_bffd.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,25 @@
66

77

88
class TestBFFD(TestCase):
9+
910
def test_nothing_happens(self):
1011
np.random.seed(0)
11-
cffd = BFFD()
12-
original_mesh_points = np.load(
13-
"tests/test_datasets/meshpoints_sphere_orig.npy")
12+
original_mesh_points = np.random.rand(100,3)
1413
A = np.random.rand(3, original_mesh_points.reshape(-1).shape[0])
15-
b = cffd.fun(original_mesh_points)
16-
cffd.fixval = b
14+
15+
b = np.mean(original_mesh_points,axis=0)
16+
cffd = BFFD(b)
1717
cffd.adjust_control_points(original_mesh_points)
1818
new_mesh_points = cffd.ffd(original_mesh_points)
19-
assert np.array_equal(original_mesh_points, new_mesh_points)
19+
assert np.linalg.norm(original_mesh_points-new_mesh_points)/np.linalg.norm(original_mesh_points)<1e-06
2020

2121
def test_constraint(self):
2222
np.random.seed(0)
23-
cffd = BFFD()
24-
cffd.read_parameters(
25-
"tests/test_datasets/parameters_test_ffd_sphere.prm")
26-
original_mesh_points = np.load(
27-
"tests/test_datasets/meshpoints_sphere_orig.npy")
28-
b = cffd.fun(original_mesh_points)
29-
cffd.fixval = b
23+
original_mesh_points = np.random.rand(100,3)
24+
A = np.random.rand(3, original_mesh_points.reshape(-1).shape[0])
25+
b = np.mean(original_mesh_points,axis=0)+0.02*np.random.rand(3)
26+
cffd = BFFD(b)
27+
cffd.read_parameters('tests/test_datasets/parameters_test_cffd.prm')
3028
cffd.adjust_control_points(original_mesh_points)
3129
new_mesh_points = cffd.ffd(original_mesh_points)
32-
assert np.isclose(np.linalg.norm(cffd.fun(new_mesh_points) - b),
33-
np.array([0.0]))
30+
assert np.linalg.norm(b-np.mean(new_mesh_points,axis=0))/np.linalg.norm(b)<1e-06

0 commit comments

Comments
 (0)