Skip to content

Commit 43bd627

Browse files
added cffd and variations
1 parent dcf5ee8 commit 43bd627

File tree

6 files changed

+784
-0
lines changed

6 files changed

+784
-0
lines changed

pygem/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,16 @@
88
"idw",
99
"rbf_factory",
1010
"custom_deformation",
11+
"cffd"
12+
"bffd"
13+
"vffd"
1114
]
1215

1316
from .deformation import Deformation
1417
from .ffd import FFD
18+
from .cffd import CFFD
19+
from .bffd import BFFD
20+
from .vffd import VFFD
1521
from .rbf import RBF
1622
from .idw import IDW
1723
from .rbf_factory import RBFFactory

pygem/bffd.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from pygem.cffd import CFFD
2+
import numpy as np
3+
4+
"""
5+
Utilities for performing Barycenter Free Form Deformation (BFFD).
6+
7+
:Theoretical Insight:
8+
It performs Free Form Deformation while trying to enforce the barycenter to be a certain value specified by the user.
9+
The constraint is enforced exactly (up to numerical errors).
10+
For details see the mother and the grandmother clases.
11+
12+
13+
"""
14+
15+
16+
17+
class BFFD(CFFD):
18+
'''
19+
Class that handles the Barycenter Free Form Deformation on the mesh points.
20+
21+
:Example:
22+
23+
>>> from pygem import BFFD
24+
>>> import numpy as np
25+
>>> bffd = BFFD()
26+
>>> bffd.read_parameters('tests/test_datasets/parameters_test_ffd_sphere.prm')
27+
>>> original_mesh_points = np.load('tests/test_datasets/meshpoints_sphere_orig.npy')
28+
>>> b=bffd.linconstraint(original_mesh_points)
29+
>>> bffd.valconstraint=b
30+
>>> bffd.indices=np.arange(np.prod(bffd.n_control_points)*3).tolist()
31+
>>> bffd.M=np.eye(len(bffd.indices))
32+
>>> new_mesh_points = bffd(original_mesh_points)
33+
>>> assert np.isclose(np.linalg.norm(bffd.linconstraint(new_mesh_points)-b),np.array([0.]))
34+
35+
36+
'''
37+
def __init__(self, n_control_points=None):
38+
super().__init__(n_control_points)
39+
def linfun(x):
40+
return np.mean(x.reshape(-1,3),axis=0)
41+
42+
self.linconstraint=linfun
43+
44+
def __call__(self, src_pts):
45+
return super().__call__(src_pts)
46+
47+
if __name__ == "__main__":
48+
from pygem import BFFD
49+
import numpy as np
50+
bffd = BFFD()
51+
bffd.read_parameters('tests/test_datasets/parameters_test_ffd_sphere.prm')
52+
original_mesh_points = np.load('tests/test_datasets/meshpoints_sphere_orig.npy')
53+
b=bffd.linconstraint(original_mesh_points)
54+
bffd.valconstraint=b
55+
bffd.indices=np.arange(np.prod(bffd.n_control_points)*3).tolist()
56+
bffd.M=np.eye(len(bffd.indices))
57+
new_mesh_points = bffd(original_mesh_points)
58+
assert np.isclose(np.linalg.norm(bffd.linconstraint(new_mesh_points)-b),np.array([0.]))

pygem/cffd.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
from pygem.ffd import FFD
2+
import numpy as np
3+
4+
"""
5+
Utilities for performing Constrained Free Form Deformation (CFFD).
6+
7+
:Theoretical Insight:
8+
It performs Free Form Deformation while trying to enforce a costraint of the form F(x)=c.
9+
The constraint is enforced exactly (up to numerical errors) if and only if the function is linear.
10+
For details on Free Form Deformation see the mother class.
11+
12+
13+
"""
14+
15+
16+
class CFFD(FFD):
17+
"""
18+
Class that handles the Constrained Free Form Deformation on the mesh points.
19+
20+
:cvar callable linconstraint: it defines the F of the constraint F(x)=c.
21+
22+
:cvar numpy.ndarray valconstraint: it defines the c of the constraint F(x)=c.
23+
:param list indices: it defines the indices of the control points
24+
that are moved to enforce the constraint. The control index is obtained by doing:
25+
all_indices=np.arange(n_x*n_y*n_z*3).reshape(n_x,n_y,n_z,3).tolist().
26+
:cvar numpy.ndarray M: a SDP weigth matrix. It must be of size len(indices) x len(indices).
27+
28+
29+
:Example:
30+
31+
>>> from pygem import CFFD
32+
>>> import numpy as np
33+
>>> cffd = CFFD()
34+
>>> cffd.read_parameters('tests/test_datasets/parameters_test_ffd_sphere.prm')
35+
>>> original_mesh_points = np.load('tests/test_datasets/meshpoints_sphere_orig.npy')
36+
>>> A=np.random.rand(3,original_mesh_points.reshape(-1).shape[0])
37+
>>> def fun(x):
38+
>>> x=x.reshape(-1)
39+
>>> return A@x
40+
>>> b=fun(original_mesh_points)
41+
>>> cffd.linconstraint=fun
42+
>>> cffd.valconstraint=b
43+
>>> cffd.indices=np.arange(np.prod(cffd.n_control_points)*3).tolist()
44+
>>> cffd.M=np.eye(len(cffd.indices))
45+
>>> new_mesh_points = cffd(original_mesh_points)
46+
>>> assert np.isclose(np.linalg.norm(fun(new_mesh_points)-b),np.array([0.]))
47+
48+
"""
49+
def __init__(self, n_control_points=None):
50+
super().__init__(n_control_points)
51+
self.linconstraint=None
52+
self.valconstraint=None
53+
self.indices=None
54+
self.M=None
55+
56+
def __call__(self, src_pts):
57+
saved_parameters=self._save_parameters()
58+
A,b=self._compute_linear_map(src_pts,saved_parameters.copy())
59+
d=A@saved_parameters[self.indices]+b
60+
deltax=np.linalg.inv(self.M)@A.T@np.linalg.inv((A@np.linalg.inv(self.M)@A.T))@(self.valconstraint-d)
61+
saved_parameters[self.indices]=saved_parameters[self.indices]+deltax
62+
self._load_parameters(saved_parameters)
63+
return self.ffd(src_pts)
64+
65+
def ffd(self,src_pts):
66+
'''
67+
Performs Classic Free Form Deformation.
68+
69+
:param np.ndarray src_pts
70+
:return the deformed points
71+
:rtype: numpy.ndarray
72+
'''
73+
return super().__call__(src_pts)
74+
75+
76+
def _save_parameters(self):
77+
'''
78+
Saves the FFD control points in an array of shape [n_x,ny,nz,3]
79+
:return the FFD control points in an array of shape [n_x,ny,nz,3]
80+
:rtype: numpy.ndarray
81+
'''
82+
tmp=np.zeros([*self.n_control_points,3])
83+
tmp[:,:,:,0]=self.array_mu_x
84+
tmp[:,:,:,1]=self.array_mu_y
85+
tmp[:,:,:,2]=self.array_mu_z
86+
return tmp.reshape(-1)
87+
88+
def _load_parameters(self,tmp):
89+
'''
90+
Loads the FFD control points from an array of shape [n_x,ny,nz,3]
91+
:param np.ndarray tmp
92+
:rtype: None
93+
'''
94+
tmp=tmp.reshape(*self.n_control_points,3)
95+
self.array_mu_x=tmp[:,:,:,0]
96+
self.array_mu_y=tmp[:,:,:,1]
97+
self.array_mu_z=tmp[:,:,:,2]
98+
99+
100+
# I see that a similar function already exists in pygem.utils, but it does not work for inputs and outputs of different dimensions
101+
def _compute_linear_map(self,src_pts,saved_parameters):
102+
'''
103+
Computes the coefficient and the intercept of the linear map from the control points to the output
104+
105+
:param np.ndarray src_pts
106+
:param np.ndarray saved_parameters
107+
:return a tuple containing the coefficient and the intercept
108+
:rtype tuple(np.ndarray,np.ndarray)
109+
110+
'''
111+
112+
113+
saved_parameters_bak=saved_parameters.copy()
114+
n_indices=len(self.indices)
115+
inputs=np.zeros([n_indices+1,n_indices+1])
116+
outputs=np.zeros([n_indices+1,self.valconstraint.shape[0]])
117+
tmp=saved_parameters_bak[self.indices].reshape(1,-1)
118+
inputs[0]=np.hstack([tmp, np.ones((tmp.shape[0], 1))])
119+
saved_parameters[self.indices]=tmp
120+
self._load_parameters(saved_parameters)
121+
def_pts=super().__call__(src_pts)
122+
outputs[0]=self.linconstraint(def_pts)
123+
for i in range(1,n_indices+1):
124+
tmp=np.eye(n_indices)[i%n_indices]
125+
tmp=tmp.reshape(1,-1)
126+
inputs[i]=np.hstack([tmp, np.ones((tmp.shape[0], 1))])
127+
saved_parameters[self.indices]=tmp
128+
self._load_parameters(saved_parameters)
129+
def_pts=super().__call__(src_pts)
130+
outputs[i]=self.linconstraint(def_pts)
131+
self._load_parameters(saved_parameters_bak)
132+
sol=np.linalg.lstsq(inputs,outputs,rcond=None)
133+
A=sol[0].T[:,:-1]
134+
b=sol[0].T[:,-1]
135+
self._load_parameters(saved_parameters_bak)
136+
return A,b

pygem/vffd.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from pygem.cffd import CFFD
2+
import numpy as np
3+
from copy import deepcopy
4+
5+
"""
6+
Utilities for performing Volume Free Form Deformation (BFFD).
7+
8+
:Theoretical Insight:
9+
It performs Free Form Deformation while trying to enforce the volume to be a certain value specified by the user.
10+
The constraint is enforced exactly (up to numerical errors).
11+
For details see the mother and the grandmother clases.
12+
13+
"""
14+
15+
16+
class VFFD(CFFD):
17+
'''
18+
Class that handles the Volumetric Free Form Deformation on the mesh points.
19+
20+
cvar np.ndarray vweight: specifies the weight of every step of the volume enforcement.
21+
22+
:Example:
23+
24+
>>> from pygem import VFFD
25+
>>> import numpy as np
26+
>>> import meshio
27+
>>> mesh = meshio.read('tests/test_datasets/test_sphere.stl')
28+
>>> original_mesh_points=mesh.points
29+
>>> triangles = mesh.cells_dict["triangle"]
30+
>>> vffd = VFFD(triangles,[2,2,2])
31+
>>> vffd.read_parameters('tests/test_datasets/parameters_test_ffd_sphere.prm')
32+
>>> b=vffd.linconstraint(original_mesh_points)
33+
>>> vffd.valconstraint=np.array([b])
34+
>>> vffd.indices=np.arange(np.prod(vffd.n_control_points)*3).tolist()
35+
>>> vffd.M=np.eye(len(vffd.indices))
36+
>>> new_mesh_points = vffd(original_mesh_points)
37+
>>> assert np.isclose(np.linalg.norm(vffd.linconstraint(new_mesh_points)-b),np.array([0.]))
38+
39+
'''
40+
def __init__(self,triangles ,n_control_points=None):
41+
super().__init__(n_control_points)
42+
self.triangles=triangles
43+
self.vweight=[1/3,1/3,1/3]
44+
def volume(x):
45+
x=x.reshape(-1,3)
46+
mesh=x[self.triangles]
47+
return np.sum(np.linalg.det(mesh))
48+
self.linconstraint=volume
49+
50+
51+
def __call__(self,src_pts):
52+
self.vweight=np.abs(self.vweight)/np.sum(np.abs(self.vweight))
53+
indices_bak=deepcopy(self.indices)
54+
self.indices=np.array(self.indices)
55+
indices_x=self.indices[self.indices%3==0].tolist()
56+
indices_y=self.indices[self.indices%3==1].tolist()
57+
indices_z=self.indices[self.indices%3==2].tolist()
58+
indexes=[indices_x,indices_y,indices_z]
59+
diffvolume=self.valconstraint-self.linconstraint(self.ffd(src_pts))
60+
for i in range(3):
61+
self.indices=indexes[i]
62+
self.M=np.eye(len(self.indices))
63+
self.valconstraint=self.linconstraint(self.ffd(src_pts))+self.vweight[i]*(diffvolume)
64+
_=super().__call__(src_pts)
65+
tmp=super().__call__(src_pts)
66+
self.indices=indices_bak
67+
return tmp
5.36 MB
Binary file not shown.

0 commit comments

Comments
 (0)