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
0 commit comments