Skip to content

Commit 3c11eb1

Browse files
authored
Merge pull request #268 from guglielmopadula/cffd
added cffd and variations
2 parents ef70b42 + 4a9d160 commit 3c11eb1

21 files changed

+26111
-8
lines changed

docs/source/_tutorials/tutorial-7-cffd.html

Lines changed: 8044 additions & 0 deletions
Large diffs are not rendered by default.

docs/source/bffd.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Barycenter Free-form Deformation
2+
=====================
3+
4+
.. currentmodule:: pygem.bffd
5+
6+
.. automodule:: pygem.bffd
7+
8+
.. autosummary::
9+
:toctree: _summaries
10+
:nosignatures:
11+
12+
.. autoclass:: pygem.bffd.BFFD
13+
:members:
14+
:special-members: __call__
15+
:private-members:
16+
:exclude-members: _abd_impl
17+
:show-inheritance:
18+
:noindex:

docs/source/cffd.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Constrained Free-form Deformation
2+
=====================
3+
4+
.. currentmodule:: pygem.cffd
5+
6+
.. automodule:: pygem.cffd
7+
8+
.. autosummary::
9+
:toctree: _summaries
10+
:nosignatures:
11+
12+
.. autoclass:: pygem.cffd.CFFD
13+
:members:
14+
:special-members: __call__
15+
:private-members:
16+
:exclude-members: _abd_impl
17+
:show-inheritance:
18+
:noindex:

docs/source/code.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ Code Documentation
55
:maxdepth: 2
66

77
ffd
8+
cffd
9+
bffd
10+
vffd
811
rbf
912
rbf_factory
1013
idw

docs/source/tutorials.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ Tutorials
44
We made some tutorial examples:
55

66
- `Tutorial 1 <tutorial-1-ffd.html>`_ shows how to apply the free-form deformation to mesh nodes.
7-
- `Turorial 2 <tutorial-2-iges.html>`_ shows how to deal with iges (CAD) files and FFD.
8-
- `Turorial 3 <tutorial-3-rbf.html>`_ shows how to apply the radial basis function to mesh nodes.
9-
- `Turorial 4 <tutorial-4-idw.html>`_ shows how to perform a deformation using the inverse distance weighting method to mesh nodes.
10-
- `Turorial 5 <tutorial-5-file.html>`_ shows how to perform a deformation to an object stored in a file.
11-
- `Turorial 6 <tutorial-6-ffd-rbf.html>`_ shows deforming a computational mesh.
12-
7+
- `Tutorial 2 <tutorial-2-iges.html>`_ shows how to deal with iges (CAD) files and FFD.
8+
- `Tutorial 3 <tutorial-3-rbf.html>`_ shows how to apply the radial basis function to mesh nodes.
9+
- `Tutorial 4 <tutorial-4-idw.html>`_ shows how to perform a deformation using the inverse distance weighting method to mesh nodes.
10+
- `Tutorial 5 <tutorial-5-file.html>`_ shows how to perform a deformation to an object stored in a file.
11+
- `Tutorial 6 <tutorial-6-ffd-rbf.html>`_ shows deforming a computational mesh.
12+
- `Tutorial 7 <tutorial-7-cffd.html>`_ shows deforming a computational mesh.

docs/source/vffd.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Volume Free-form Deformation
2+
=====================
3+
4+
.. currentmodule:: pygem.vffd
5+
6+
.. automodule:: pygem.vffd
7+
8+
.. autosummary::
9+
:toctree: _summaries
10+
:nosignatures:
11+
12+
.. autoclass:: pygem.vffd.VFFD
13+
:members:
14+
:special-members: __call__
15+
:private-members:
16+
:exclude-members: _abd_impl
17+
:show-inheritance:
18+
:noindex:

pygem/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,18 @@
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
1519
from .rbf import RBF
1620
from .idw import IDW
1721
from .rbf_factory import RBFFactory
1822
from .custom_deformation import CustomDeformation
1923
from .meta import *
24+
from .bffd import BFFD
25+
from .vffd import VFFD

pygem/bffd.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from pygem.cffd import CFFD
2+
import numpy as np
3+
class BFFD(CFFD):
4+
'''
5+
Class that handles the Barycenter Free Form Deformation on the mesh points.
6+
7+
:param list n_control_points: number of control points in the x, y, and z
8+
direction. Default is [2, 2, 2].
9+
10+
:cvar numpy.ndarray box_length: dimension of the FFD bounding box, in the
11+
x, y and z direction (local coordinate system).
12+
:cvar numpy.ndarray box_origin: the x, y and z coordinates of the origin of
13+
the FFD bounding box.
14+
:cvar numpy.ndarray n_control_points: the number of control points in the
15+
x, y, and z direction.
16+
:cvar numpy.ndarray array_mu_x: collects the displacements (weights) along
17+
x, normalized with the box length x.
18+
:cvar numpy.ndarray array_mu_y: collects the displacements (weights) along
19+
y, normalized with the box length y.
20+
:cvar numpy.ndarray array_mu_z: collects the displacements (weights) along
21+
z, normalized with the box length z.
22+
:cvar callable fun: it defines the F of the constraint F(x)=c. Default is the constant 1 function.
23+
:cvar numpy.ndarray fixval: it defines the c of the constraint F(x)=c. Default is 1.
24+
:cvar numpy.ndarray mask: a boolean tensor that tells to the class
25+
which control points can be moved, and in what direction, to enforce the constraint.
26+
The tensor has shape (n_x,n_y,n_z,3), where the last dimension indicates movement
27+
on x,y,z respectively. Default is all true.
28+
29+
:Example:
30+
31+
>>> from pygem import BFFD
32+
>>> b = np.random.rand(3)
33+
>>> bffd = BFFD(b, [2, 2, 2])
34+
>>> bffd.read_parameters('tests/test_datasets/parameters_test_cffd')
35+
>>> original_mesh_points = np.load("tests/test_datasets/test_sphere_cffd.npy")
36+
>>> bffd.adjust_control_points(original_mesh_points[:-4])
37+
>>> assert np.isclose(np.linalg.norm(bffd.fun(bffd.ffd(original_mesh_points[:-4])) - b), np.array([0.]))
38+
>>> new_mesh_points = bffd.ffd(original_mesh_points)
39+
'''
40+
41+
def __init__(self, fixval=None, n_control_points=None, ffd_mask=None):
42+
super().__init__(fixval, None, n_control_points, ffd_mask, None)
43+
44+
def linfun(x):
45+
return np.mean(x.reshape(-1, 3), axis=0)
46+
47+
self.fun = linfun
48+
self.fixval = fixval
49+
self.fun_mask = np.array([[True, False, False], [False, True, False],
50+
[False, False, True]])
51+
52+

pygem/cffd.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
"""
2+
Utilities for performing Constrained Free Form Deformation (CFFD).
3+
4+
:Theoretical Insight:
5+
6+
It performs Free Form Deformation while trying to enforce a costraint of the form F(x)=c.
7+
The constraint is enforced exactly (up to numerical errors) if and only if the function is linear.
8+
For details on Free Form Deformation see the mother class.
9+
10+
"""
11+
12+
from pygem.ffd import FFD
13+
import numpy as np
14+
from scipy.optimize import LinearConstraint, differential_evolution
15+
16+
17+
class CFFD(FFD):
18+
"""
19+
Class that handles the Constrained Free Form Deformation on the mesh points.
20+
21+
:param list n_control_points: number of control points in the x, y, and z
22+
direction. Default is [2, 2, 2].
23+
: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.
24+
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.
25+
:cvar numpy.ndarray box_length: dimension of the FFD bounding box, in the
26+
x, y and z direction (local coordinate system).
27+
:cvar numpy.ndarray box_origin: the x, y and z coordinates of the origin of
28+
the FFD bounding box.
29+
:cvar numpy.ndarray n_control_points: the number of control points in the
30+
x, y, and z direction.
31+
:cvar numpy.ndarray array_mu_x: collects the displacements (weights) along
32+
x, normalized with the box length x.
33+
:cvar numpy.ndarray array_mu_y: collects the displacements (weights) along
34+
y, normalized with the box length y.
35+
:cvar numpy.ndarray array_mu_z: collects the displacements (weights) along
36+
z, normalized with the box length z.
37+
:cvar callable fun: it defines the F of the constraint F(x)=c. Default is the constant 1 function.
38+
:cvar numpy.ndarray fixval: it defines the c of the constraint F(x)=c. Default is 1.
39+
:cvar numpy.ndarray ffd_mask: a boolean tensor that tells to the class
40+
which control points can be moved, and in what direction, to enforce the constraint.
41+
The tensor has shape (n_x,n_y,n_z,3), where the last dimension indicates movement
42+
on x,y,z respectively. Default is all true.
43+
:cvar numpy.ndarray fun_mask: a boolean tensor that tells to the class
44+
on which axis which constraint depends on. The tensor has shape (n_cons,3), where the last dimension indicates dependency on
45+
on x,y,z respectively. Default is all true. It used only in the triaffine mode.
46+
47+
:Example:
48+
>>> from pygem import CFFD
49+
>>> import numpy as np
50+
>>> original_mesh_points = np.load("tests/test_datasets/test_sphere_cffd.npy")
51+
>>> A = np.random.rand(3, original_mesh_points[:-4].reshape(-1).shape[0])
52+
>>> fun = lambda x: A @ x.reshape(-1)
53+
>>> b = np.random.rand(3)
54+
>>> cffd = CFFD(b, fun, [2, 2, 2])
55+
>>> cffd.read_parameters('tests/test_datasets/parameters_test_cffd.prm')
56+
>>> cffd.adjust_control_points(original_mesh_points[:-4])
57+
>>> assert np.isclose(np.linalg.norm(fun(cffd.ffd(original_mesh_points[:-4])) - b), np.array([0.]), atol = 1e-06)
58+
>>> new_mesh_points = cffd.ffd(original_mesh_points)
59+
60+
"""
61+
def __init__(self,
62+
fixval,
63+
fun,
64+
n_control_points=None,
65+
ffd_mask=None,
66+
fun_mask=None):
67+
super().__init__(n_control_points)
68+
69+
if ffd_mask is None:
70+
self.ffd_mask = np.full((*self.n_control_points, 3),
71+
True,
72+
dtype=bool)
73+
else:
74+
self.ffd_mask = ffd_mask
75+
76+
self.num_cons = len(fixval)
77+
self.fun = fun
78+
self.fixval = fixval
79+
if fun_mask is None:
80+
self.fun_mask = np.full((self.num_cons, 3), True, dtype=bool)
81+
else:
82+
self.fun_mask = fun_mask
83+
84+
def adjust_control_points(self, src_pts):
85+
'''
86+
Adjust the FFD control points such that fun(ffd(src_pts))=fixval
87+
88+
:param np.ndarray src_pts: the points whose deformation we want to be
89+
constrained.
90+
:rtype: None.
91+
'''
92+
hyper_param = self.fun_mask.copy().astype(float)
93+
hyper_param = hyper_param / np.sum(hyper_param, axis=1)
94+
mask_bak = self.ffd_mask.copy()
95+
fixval_bak = self.fixval.copy()
96+
diffvolume = self.fixval - self.fun(self.ffd(src_pts))
97+
for i in range(3):
98+
self.ffd_mask = np.full((*self.n_control_points, 3),
99+
False,
100+
dtype=bool)
101+
self.ffd_mask[:, :, :, i] = mask_bak[:, :, :, i].copy()
102+
self.fixval = self.fun(
103+
self.ffd(src_pts)) + hyper_param[:, i] * (diffvolume)
104+
saved_parameters = self._save_parameters()
105+
indices = np.arange(np.prod(self.n_control_points) *
106+
3)[self.ffd_mask.reshape(-1)]
107+
A, b = self._compute_linear_map(src_pts, saved_parameters.copy(),
108+
indices)
109+
A = A[self.fun_mask[:, i].reshape(-1), :]
110+
b = b[self.fun_mask[:, i].reshape(-1)]
111+
d = A @ saved_parameters[indices] + b
112+
fixval = self.fixval[self.fun_mask[:, i].reshape(-1)]
113+
deltax = np.linalg.multi_dot([
114+
A.T,
115+
np.linalg.inv(np.linalg.multi_dot([A, A.T])), (fixval - d)
116+
])
117+
saved_parameters[indices] = saved_parameters[indices] + deltax
118+
self._load_parameters(saved_parameters)
119+
self.ffd_mask = mask_bak.copy()
120+
self.fixval = fixval_bak.copy()
121+
122+
def ffd(self, src_pts):
123+
'''
124+
Performs Classic Free Form Deformation.
125+
126+
:param np.ndarray src_pts: the points to deform.
127+
:return: the deformed points.
128+
:rtype: numpy.ndarray
129+
'''
130+
return super().__call__(src_pts)
131+
132+
def _save_parameters(self):
133+
'''
134+
Saves the FFD control points in an array of shape [n_x,ny,nz,3].
135+
136+
:return: the FFD control points in an array of shape [n_x,ny,nz,3].
137+
:rtype: numpy.ndarray
138+
'''
139+
tmp = np.zeros([*self.n_control_points, 3])
140+
tmp[:, :, :, 0] = self.array_mu_x
141+
tmp[:, :, :, 1] = self.array_mu_y
142+
tmp[:, :, :, 2] = self.array_mu_z
143+
return tmp.reshape(-1)
144+
145+
def _load_parameters(self, tmp):
146+
'''
147+
Loads the FFD control points from an array of shape [n_x,ny,nz,3].
148+
149+
:param np.ndarray tmp: the array of FFD control points.
150+
:rtype: None
151+
'''
152+
tmp = tmp.reshape(*self.n_control_points, 3)
153+
self.array_mu_x = tmp[:, :, :, 0]
154+
self.array_mu_y = tmp[:, :, :, 1]
155+
self.array_mu_z = tmp[:, :, :, 2]
156+
157+
def _compute_linear_map(self, src_pts, saved_parameters, indices):
158+
'''
159+
Computes the coefficient and the intercept of the linear map from the control points to the output.
160+
161+
:param np.ndarray src_pts: the points to deform.
162+
:param np.ndarray saved_parameters: the array of FFD control points.
163+
:return: a tuple containing the coefficient and the intercept.
164+
:rtype: tuple(np.ndarray,np.ndarray)
165+
'''
166+
n_indices = len(indices)
167+
inputs = np.zeros([n_indices + 1, n_indices + 1])
168+
outputs = np.zeros([n_indices + 1, self.fixval.shape[0]])
169+
np.random.seed(0)
170+
for i in range(n_indices +
171+
1): ##now we generate the interpolation points
172+
tmp = np.random.rand(1, n_indices)
173+
tmp = tmp.reshape(1, -1)
174+
inputs[i] = np.hstack([tmp, np.ones(
175+
(tmp.shape[0], 1))]) #dependent variable
176+
saved_parameters[indices] = tmp
177+
self._load_parameters(
178+
saved_parameters
179+
) #loading the depent variable as a control point
180+
def_pts = super().__call__(
181+
src_pts) #computing the deformation with the dependent variable
182+
outputs[i] = self.fun(def_pts) #computing the independent variable
183+
sol = np.linalg.lstsq(inputs, outputs,
184+
rcond=None) #computation of the linear map
185+
A = sol[0].T[:, :-1] #coefficient
186+
b = sol[0].T[:, -1] #intercept
187+
return A, b

0 commit comments

Comments
 (0)