Hi,
I would like to optimize a linear elasticity code to run faster by reusing the LU decomposition used by PETSc.
Below I have a version of my code that runs using the function LinearProblem(), and another version where I assemble the Lu=b system.
- What I noticed is that to assemble A using assemble_matrix() you need the boundary conditions, why is this? Is L modified? Once I can move the assembly of L outside the loop, how could I reuse the LU decomposition from PETSc?
- In the function solve_fea() there is also a step where I fix the displacements along some directions. From what I remember this means the solution is also Lu=b, but with the rows and columns corresponding to the degrees of freedom are removed. I think this also means I have to remove those rows and columns from the decomposition, and then map them back the full problem. Is this correct? How could I do this in PETSc?
A self contained running code is shown below.
Thank you,
Alex
import numpy as np
import time
from mpi4py import MPI
from petsc4py import PETSc
import ufl
from dolfinx import mesh, fem, io, default_scalar_type
from dolfinx.fem.petsc import assemble_matrix, assemble_vector, apply_lifting, set_bc, LinearProblem
r_loading = 2.0e-3
def in_sphere(x, point, r_range):
return ((x[0]-point[0])**2 + (x[1]-point[1])**2 + (x[2]-point[2])**2) < r_range**2
def mark_boundaries(domain, facet_node,grasp_node,grasp_node_normal):
dim = domain.topology.dim
fdim = dim - 1
domain.topology.create_connectivity(fdim, dim)
def left_facets(xq):
return in_sphere(xq, facet_node, r_loading)
def right_facets(xq):
return in_sphere(xq, grasp_node, r_loading)
facets_left = mesh.locate_entities(domain, fdim, left_facets)
facets_right = mesh.locate_entities(domain, fdim, right_facets)
facet_indices = np.concatenate([facets_left, facets_right])
facet_markers = np.concatenate([np.full_like(facets_left, 1, dtype=np.int32),
np.full_like(facets_right, 2, dtype=np.int32)])
# Sort by facet index
sort_perm = np.argsort(facet_indices)
facet_indices = facet_indices[sort_perm]
facet_markers = facet_markers[sort_perm]
mt = mesh.meshtags(domain, fdim, facet_indices, facet_markers)
return mt
def elasticity_parameters(E=10.0e4, nu=0.20):
"""Lamé parameters from E, nu."""
lambda_ = E * nu / ((1.0 + nu) * (1.0 - 2.0 * nu))
mu = E * 0.5 / (1.0 + nu)
return lambda_, mu
def epsilon(u):
return ufl.sym(ufl.grad(u))
def sigma(u, lambda_, mu):
return lambda_ * ufl.nabla_div(u) * ufl.Identity(len(u)) + 2.0 * mu * epsilon(u)
def build_primal_problem(domain, facet_tags, lambda_, mu, node_normal, lock_num,
p_traction=1.0/(np.pi*r_loading**2), body_force=(0.0, 0.0, -3941.0*9.8)):
dim = domain.topology.dim
fdim = dim - 1
ds = ufl.Measure("ds", domain=domain, subdomain_data=facet_tags)
V = fem.functionspace(domain, ("Lagrange", 1, (domain.geometry.dim,)))
u = ufl.TrialFunction(V)
v = ufl.TestFunction(V)
f_vec = fem.Constant(domain, default_scalar_type(body_force))
# Traction direction: push along +x (change sign if needed)
T = fem.Constant(domain, -p_traction * default_scalar_type((node_normal[0], node_normal[1], node_normal[2])))
a = ufl.inner(sigma(u, lambda_, mu), epsilon(v)) * ufl.dx
L = ufl.dot(f_vec, v) * ufl.dx + ufl.dot(T, v) * ds(2)
# Dirichlet BC on left jaw (tag 1)
left_facets = facet_tags.find(1)
domain.topology.create_connectivity(fdim, dim)
left_dofs = fem.locate_dofs_topological(V, fdim, left_facets)
u_D = np.array((0.0, 0.0, 0.0), dtype=default_scalar_type)
bc_left = fem.dirichletbc(u_D, left_dofs, V)
# # Dirichlet BC to stop deflection normal to x (lock_num == 1) or y (lock_num == 2) axis
# if lock_num == 1:
# right_facets = facet_tags.find(2)
# right_dofs1 = fem.locate_dofs_topological(V.sub(1), fdim, right_facets)
# bc_1 = fem.dirichletbc(0.0, right_dofs1, V.sub(1))
# right_dofs2 = fem.locate_dofs_topological(V.sub(2), fdim, right_facets)
# bc_2 = fem.dirichletbc(0.0, right_dofs2, V.sub(2))
# return V, a, L, [bc_left, bc_1, bc_2]
# if lock_num == 2:
# right_facets = facet_tags.find(2)
# right_dofs0 = fem.locate_dofs_topological(V.sub(0), fdim, right_facets)
# bc_1 = fem.dirichletbc(0.0, right_dofs0, V.sub(0))
# right_dofs2 = fem.locate_dofs_topological(V.sub(2), fdim, right_facets)
# bc_2 = fem.dirichletbc(0.0, right_dofs2, V.sub(2))
# return V, a, L, [bc_left, bc_1, bc_2]
return V, a, L, [bc_left]
def array_dist_squared(x, p):
return (x[:,0]-p[0])**2 + (x[:,1]-p[1])**2 + (x[:,2]-p[2])**2
def fea_solve(domain, facet_node, grasp_node, grasp_node_normal, lock_num):
lambda_, mu = elasticity_parameters()
facet_tags = mark_boundaries(domain, facet_node,grasp_node,grasp_node_normal)
Vp, a_p, L_p, bcs_p = build_primal_problem(domain, facet_tags, lambda_, mu, grasp_node_normal, lock_num)
problem = LinearProblem(a_p, L_p, bcs=bcs_p,
petsc_options_prefix="linear_problem",
petsc_options={"ksp_type": "preonly", "pc_type": "lu", "pc_factor_mat_solver_type": "mumps"})
uh = problem.solve()
# Get displacement at node where load is applied
dof_coords = Vp.tabulate_dof_coordinates()
distances = np.linalg.norm(dof_coords - grasp_node, axis=1)
dof_index = np.argmin(distances)
u_node = np.sqrt(uh.x.array[3*dof_index]**2 + uh.x.array[3*dof_index+1]**2 + uh.x.array[3*dof_index+2]**2)
return domain, uh, u_node
# Part FEA
##################################################################
length = 20.0e-3
width = 10.0e-3
grasp_facet_nodes = np.array([[0.0, width/2, width/2],
[0.0, width/2, width/2 + 1.0e-3],
[0.0, width/2, width/2 + 2.0e-3]])
grasp_nodes2 = np.array([[length, width/2, width/2],
[length, width/2, width/2 + 1.0e-3],
[length, width/2, width/2 + 2.0e-3]])
grasp_node_normal = np.array([1.0, 0.0, 0.0 ])
cell_size = 0.5e-3
n_l = int(length/cell_size)
n_w = int(width /cell_size)
lock_num = 1
domain = mesh.create_box(MPI.COMM_WORLD, ((0.0, 0.0, 0.0), (length,width,width)), (n_l,n_w,n_w), mesh.CellType.tetrahedron)
for n in range(grasp_facet_nodes.shape[0]):
t_start = time.perf_counter()
domain_f, uh, u_node = fea_solve(domain, grasp_facet_nodes[n,:], grasp_nodes2[n,:], grasp_node_normal, lock_num)
t_end = time.perf_counter()
print(f"FEA elapsed time: {t_end - t_start:.6f} seconds")
with io.XDMFFile(domain_f.comm, f"max_p_stress_part_loading_{n+1}.xdmf", "w") as xdmf:
xdmf.write_mesh(domain_f)
uh.name = "u"
xdmf.write_function(uh)
print(u_node)
print('\n')
# Common assembly steps
t_start = time.perf_counter()
lambda_, mu = elasticity_parameters()
dim = domain.topology.dim
fdim = dim - 1
Vp = fem.functionspace(domain, ("Lagrange", 1, (domain.geometry.dim,)))
u = ufl.TrialFunction(Vp)
v = ufl.TestFunction(Vp)
body_force=(0.0, 0.0, -3941.0*9.8)
f_vec = fem.Constant(domain, default_scalar_type(body_force))
# Traction direction: push along +x (change sign if needed)
p_traction=1.0/(np.pi*r_loading**2)
T = fem.Constant(domain, -p_traction * default_scalar_type((grasp_node_normal[0], grasp_node_normal[1], grasp_node_normal[2])))
a_p = ufl.inner(sigma(u, lambda_, mu), epsilon(v)) * ufl.dx
domain.topology.create_connectivity(fdim, dim)
u_D = np.array((0.0, 0.0, 0.0), dtype=default_scalar_type)
t_end = time.perf_counter()
print(f"Common assembly time: {t_end - t_start:.6f} seconds")
for n in range(grasp_facet_nodes.shape[0]):
t_start = time.perf_counter()
facet_tags = mark_boundaries(domain, grasp_facet_nodes[n,:],grasp_nodes2[n,:],grasp_node_normal)
ds = ufl.Measure("ds", domain=domain, subdomain_data=facet_tags)
L_p = ufl.dot(f_vec, v) * ufl.dx + ufl.dot(T, v) * ds(2)
# Dirichlet BC on left jaw (tag 1)
left_facets = facet_tags.find(1)
left_dofs = fem.locate_dofs_topological(Vp, fdim, left_facets)
bcs = fem.dirichletbc(u_D, left_dofs, Vp)
bcs_p = [bcs]
"""
Solve linear system A u = b with PETSc LU.
"""
a_form = fem.form(a_p)
L_form = fem.form(L_p)
A = assemble_matrix(a_form, bcs=bcs_p)
A.assemble()
b = assemble_vector(L_form)
b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
set_bc(b, bcs_p)
uh = fem.Function(Vp)
solver = PETSc.KSP().create(domain.comm)
solver.setType(PETSc.KSP.Type.PREONLY)
solver.getPC().setType(PETSc.PC.Type.LU)
solver.getPC().setFactorSolverType('mumps')
solver.setOperators(A)
solver.solve(b, uh.x.petsc_vec)
uh.x.scatter_forward()
# Get displacement at node where load is applied
dof_coords = Vp.tabulate_dof_coordinates()
distances = np.linalg.norm(dof_coords - grasp_nodes2[n,:], axis=1)
dof_index = np.argmin(distances)
u_node = np.sqrt(uh.x.array[3*dof_index]**2 + uh.x.array[3*dof_index+1]**2 + uh.x.array[3*dof_index+2]**2)
t_end = time.perf_counter()
print(f"FEA elapsed time: {t_end - t_start:.6f} seconds")
with io.XDMFFile(domain_f.comm, f"max_p_stress2_part_loading_{n+1}.xdmf", "w") as xdmf:
xdmf.write_mesh(domain_f)
uh.name = "u"
xdmf.write_function(uh)
print(u_node)