Skip to content

Commit dceabc3

Browse files
committed
Add quadratic deformation regularizers to shape matching restriction operator
1 parent 62841f2 commit dceabc3

File tree

2 files changed

+80
-51
lines changed

2 files changed

+80
-51
lines changed

python/examples/multigrid/linear_restriction_prolongation.py

Lines changed: 66 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,16 @@ def min_max_eigs(A, n=10):
1616
return lmin, lmax
1717

1818

19-
def shape_function_matrix(mesh, Xg):
19+
def laplacian_energy(mesh, k=1, dims=1):
20+
L = -pbat.fem.laplacian(mesh, dims=dims)[0]
21+
M = None if k == 1 else pbat.fem.mass_matrix(mesh, dims=1, lump=True)
22+
U = L
23+
for i in range(k-1):
24+
U = U @ M @ L
25+
return U
26+
27+
28+
def shape_function_matrix(mesh, Xg, dims=1):
2029
nevalpts = Xg.shape[1]
2130
# Unfortunately, we have to perform the copy here...
2231
# mesh.X and mesh.E are defined via def_property in pybind11, and
@@ -35,11 +44,13 @@ def shape_function_matrix(mesh, Xg):
3544
cols = mesh.E[:, e].flatten(order='F')
3645
nnodes = mesh.X.shape[1]
3746
N = sp.sparse.coo_matrix((data, (rows, cols)), shape=(nevalpts, nnodes))
47+
if dims > 1:
48+
N = sp.sparse.kron(N, sp.sparse.eye(dims))
3849
return N.asformat('csc')
3950

4051

4152
class BaseFemFunctionTransferOperator():
42-
def __init__(self, MD: pbat.fem.Mesh, MS: pbat.fem.Mesh, MT: pbat.fem.Mesh):
53+
def __init__(self, MD: pbat.fem.Mesh, MS: pbat.fem.Mesh, MT: pbat.fem.Mesh, H):
4354
"""Operator for transferring FEM discretized functions from a source
4455
mesh MS to a target mesh MT, given the domain MD.
4556
@@ -48,36 +59,40 @@ def __init__(self, MD: pbat.fem.Mesh, MS: pbat.fem.Mesh, MT: pbat.fem.Mesh):
4859
MS (pbat.fem.Mesh): Source mesh
4960
MT (pbat.fem.Mesh): Target mesh
5061
"""
51-
nelems = MT.E.shape[1]
62+
nelems = MD.E.shape[1]
5263
quadrature_order = 2*max(MS.order, MT.order)
53-
Xg = MT.quadrature_points(quadrature_order)
54-
wg = np.tile(MT.quadrature_weights(quadrature_order), nelems)
64+
Xg = MD.quadrature_points(quadrature_order)
65+
wg = np.tile(MD.quadrature_weights(quadrature_order), nelems)
5566
Ig = sp.sparse.diags(wg)
56-
NS = shape_function_matrix(MS, Xg)
57-
NT = shape_function_matrix(MT, Xg)
67+
Ig = sp.sparse.kron(Ig, sp.sparse.eye(MT.dims))
68+
NS = shape_function_matrix(MS, Xg, dims=MT.dims)
69+
NT = shape_function_matrix(MT, Xg, dims=MT.dims)
5870
A = (NT.T @ Ig @ NT).asformat('csc')
5971
P = NT.T @ Ig @ NS
6072
self.Ig = Ig
6173
self.NS = NS
6274
self.NT = NT
6375
self.A = A
6476
self.P = P
77+
self.U = laplacian_energy(MT, dims=MT.dims)
78+
self.H = H
6579

6680
def __matmul__(self, X):
6781
pass
6882

6983

7084
class CholFemFunctionTransferOperator(BaseFemFunctionTransferOperator):
71-
def __init__(self, MD: pbat.fem.Mesh, MS: pbat.fem.Mesh, MT: pbat.fem.Mesh):
72-
super().__init__(MD, MS, MT)
85+
def __init__(self, MD: pbat.fem.Mesh, MS: pbat.fem.Mesh, MT: pbat.fem.Mesh, H, lreg=5, hreg=1):
86+
super().__init__(MD, MS, MT, H)
7387
n = self.A.shape[0]
74-
lmin, lmax = min_max_eigs(self.A, n=1)
88+
A = self.A + lreg*self.U + hreg*self.H
89+
lmin, lmax = min_max_eigs(A, n=1)
7590
tau = 0.
7691
if lmin[0] <= 0:
7792
# Regularize A (due to positive semi-definiteness)
7893
tau = abs(lmin[0]) + 1e-10
7994
tau = sp.sparse.diags(np.full(n, tau))
80-
AR = self.A + tau
95+
AR = A + tau
8196
solver = pbat.math.linalg.SolverBackend.Eigen
8297
self.Ainv = pbat.math.linalg.chol(AR, solver=solver)
8398
self.Ainv.compute(AR)
@@ -89,36 +104,34 @@ def __matmul__(self, B):
89104

90105

91106
class RankKApproximateFemFunctionTransferOperator(BaseFemFunctionTransferOperator):
92-
def __init__(self, MD: pbat.fem.Mesh, MS: pbat.fem.Mesh, MT: pbat.fem.Mesh, modes=30):
93-
super().__init__(MD, MS, MT)
94-
modes = modes / 2
95-
Ulo, sigmalo, VTlo = sp.sparse.linalg.svds(
96-
self.Ig @ self.NT, k=modes, which='SM')
97-
Uhi, sigmahi, VThi = sp.sparse.linalg.svds(
98-
self.Ig @ self.NT, k=modes, which='LM')
99-
self.U, self.sigma, self.VT = np.hstack((Ulo, Uhi)), np.hstack(
100-
(sigmalo, sigmahi)), np.vstack((VTlo, VThi))
101-
keep = np.nonzero(self.sigma > 1e-5)[0]
102-
self.U = self.U[:, keep]
103-
self.sigma = self.sigma[keep]
104-
self.VT = self.VT[keep, :]
107+
def __init__(self, MD: pbat.fem.Mesh, MS: pbat.fem.Mesh, MT: pbat.fem.Mesh, H, lreg=5, hreg=1, modes=30):
108+
super().__init__(MD, MS, MT, H)
109+
A = self.A + lreg*self.U + hreg*self.H
110+
l, V = sp.sparse.linalg.eigsh(A, k=modes, sigma=1e-5, which='LM')
111+
keep = np.nonzero(l > 1e-5)[0]
112+
self.l, self.V = l[keep], V[:, keep]
105113

106114
def __matmul__(self, B):
107-
B = self.Ig @ self.NS @ B
108-
B = self.U.T @ B
109-
B = B / self.sigma[:, np.newaxis]
110-
X = self.VT.T @ B
115+
B = self.P @ B
116+
B = self.V.T @ B
117+
B = B @ sp.sparse.diags(1 / self.l)
118+
X = self.V @ B
111119
return X
112120

113121

114-
def linear_elastic_deformation_modes(mesh, rho, Y, nu, modes=30, sigma=-1e-5):
122+
def rest_pose_hessian(mesh, Y, nu):
115123
x = mesh.X.reshape(math.prod(mesh.X.shape), order='f')
116-
M, detJeM = pbat.fem.mass_matrix(mesh, rho=rho)
117124
energy = pbat.fem.HyperElasticEnergy.StableNeoHookean
118125
hep, detJeU, GNeU = pbat.fem.hyper_elastic_potential(
119126
mesh, Y, nu, energy=energy)
120127
hep.compute_element_elasticity(x)
121128
HU = hep.hessian()
129+
return HU
130+
131+
132+
def linear_elastic_deformation_modes(mesh, rho, Y, nu, modes=30, sigma=-1e-5):
133+
M, detJeM = pbat.fem.mass_matrix(mesh, rho=rho)
134+
HU = rest_pose_hessian(mesh, Y, nu)
122135
leigs, Veigs = sp.sparse.linalg.eigsh(
123136
HU, k=modes, M=M, sigma=sigma, which='LM')
124137
Veigs = Veigs / sp.linalg.norm(Veigs, axis=0, keepdims=True)
@@ -171,10 +184,13 @@ def signal(w: float, v: np.ndarray, t: float, c: float, k: float):
171184
# Precompute quantities
172185
w, L = linear_elastic_deformation_modes(
173186
mesh, args.rho, args.Y, args.nu, args.modes)
174-
Fldl = CholFemFunctionTransferOperator(mesh, mesh, cmesh)
187+
HC = rest_pose_hessian(cmesh, args.Y, args.nu)
188+
lreg, hreg = 0, 1e-2
189+
Fldl = CholFemFunctionTransferOperator(
190+
mesh, mesh, cmesh, HC, lreg=lreg, hreg=hreg)
175191
Krestrict = 30
176192
Frank = RankKApproximateFemFunctionTransferOperator(
177-
mesh, mesh, cmesh, modes=Krestrict)
193+
mesh, mesh, cmesh, HC, lreg=lreg, hreg=hreg, modes=Krestrict)
178194

179195
ps.set_up_dir("z_up")
180196
ps.set_front_dir("neg_y_front")
@@ -188,21 +204,33 @@ def signal(w: float, v: np.ndarray, t: float, c: float, k: float):
188204
t = 0
189205
c = 0.15
190206
k = 0.05
207+
theta = 0
208+
dtheta = np.pi/120
191209

192210
def callback():
193-
global mode, c, k
211+
global mode, c, k, theta, dtheta
194212
changed, mode = imgui.InputInt("Mode", mode)
195213
changed, c = imgui.InputFloat("Wave amplitude", c)
196214
changed, k = imgui.InputFloat("Wave frequency", k)
197215

198216
t = time.time() - t0
199-
X = V + signal(w[mode], L[:, mode],
200-
t, c, k).reshape(V.shape[0], 3)
201-
XCldl = CV + Fldl @ (X - V)
202-
XCrank = CV + Frank @ (X - V)
203-
vm.update_vertex_positions(X)
217+
218+
R = sp.spatial.transform.Rotation.from_quat(
219+
[0, np.sin(theta/2), 0, np.cos(theta/4)]).as_matrix()
220+
X = (V - V.mean(axis=0)) @ R.T + V.mean(axis=0)
221+
uf = signal(w[mode], L[:, mode], t, c, k)
222+
ur = (X - V).flatten(order="C")
223+
ut = 1
224+
u = uf + ur + ut
225+
XCldl = CV + (Fldl @ u).reshape(CV.shape)
226+
XCrank = CV + (Frank @ u).reshape(CV.shape)
227+
vm.update_vertex_positions(V + (uf + ur).reshape(V.shape))
204228
ldlvm.update_vertex_positions(XCldl)
205229
rankvm.update_vertex_positions(XCrank)
206230

231+
theta += dtheta
232+
if theta > 2*np.pi:
233+
theta = 0
234+
207235
ps.set_user_callback(callback)
208236
ps.show()

python/pbatoolkit/fem.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def laplacian(
9494
mesh,
9595
dims: int = 1,
9696
quadrature_order: int = 1,
97-
detJ: np.ndarray = None,
97+
detJe: np.ndarray = None,
9898
GNe: np.ndarray = None,
9999
as_matrix: bool = True):
100100
"""Construct an FEM Laplacian operator
@@ -103,23 +103,23 @@ def laplacian(
103103
mesh (pbat.fem.Mesh):
104104
dims (int, optional): Solution space dimensions. Defaults to 1.
105105
quadrature_order (int, optional): Polynomial order to use for operator construction. Defaults to 1.
106-
detJ (np.ndarray, optional): Jacobian determinants at quadrature points. Defaults to None.
106+
detJe (np.ndarray, optional): Jacobian determinants at quadrature points. Defaults to None.
107107
GNe (np.ndarray, optional): Shape function gradients at quadrature points. Defaults to None.
108108
as_matrix (bool, optional): Return the operator as a sparse matrix. Defaults to True.
109109
110110
Returns:
111111
(pbat.fem.Laplacian | scipy.sparse.csc_matrix, np.ndarray, np.ndarray):
112112
"""
113-
if detJ is None:
114-
detJ = _fem.jacobian_determinants(
113+
if detJe is None:
114+
detJe = _fem.jacobian_determinants(
115115
mesh, quadrature_order=quadrature_order)
116116
if GNe is None:
117117
GNe = _fem.shape_function_gradients(
118118
mesh, quadrature_order=quadrature_order)
119-
L = _fem.Laplacian(mesh, detJ, GNe, dims=dims,
119+
L = _fem.Laplacian(mesh, detJe, GNe, dims=dims,
120120
quadrature_order=quadrature_order)
121121
L = L.to_matrix() if as_matrix else L
122-
return L, detJ, GNe
122+
return L, detJe, GNe
123123

124124

125125
def mass_matrix(
@@ -162,24 +162,25 @@ def load_vector(
162162
mesh,
163163
fe: np.ndarray,
164164
quadrature_order: int = 1,
165-
detJ: np.ndarray = None,
165+
detJe: np.ndarray = None,
166166
flatten: bool = True):
167167
"""Construct an FEM load vector
168168
169169
Args:
170170
mesh (pbat.fem.Mesh):
171171
fe (np.ndarray): Uniform (|#dims| or |#dims|x1) or heterogeneous (|#dims|x|#mesh quadrature points|) load.
172172
quadrature_order (int, optional): Polynomial order to use for load vector construction. Defaults to 1.
173-
detJ (np.ndarray, optional): Jacobian determinants at quadrature points. Defaults to None.
173+
detJe (np.ndarray, optional): Jacobian determinants at quadrature points. Defaults to None.
174174
175175
Returns:
176176
(np.ndarray, np.ndarray):
177177
"""
178-
if detJ is None:
179-
detJ = _fem.jacobian_determinants(
178+
if detJe is None:
179+
detJe = _fem.jacobian_determinants(
180180
mesh, quadrature_order=quadrature_order)
181-
qgf = _fem.inner_product_weights(
182-
mesh, quadrature_order=quadrature_order).flatten(order="F")
181+
nelems = mesh.E.shape[1]
182+
wg = np.tile(mesh.quadrature_weights(quadrature_order), nelems)
183+
qgf = detJe.flatten(order="F") * wg
183184
Qf = sp.sparse.diags_array([qgf], offsets=[0])
184185
Nf = _fem.shape_function_matrix(mesh, quadrature_order=quadrature_order)
185186
if len(fe.shape) == 1:
@@ -189,4 +190,4 @@ def load_vector(
189190
f = fe @ Qf @ Nf
190191
if flatten:
191192
f = f.reshape(math.prod(f.shape), order="F")
192-
return f, detJ
193+
return f, detJe

0 commit comments

Comments
 (0)