Shape optimization of a fillet¶
import matplotlib.pyplot as plt
import torch
from tqdm import tqdm
from torchfem.examples import get_example_file
from torchfem.io import import_mesh
from torchfem.materials import IsotropicElasticityPlaneStress
torch.set_default_dtype(torch.float64)
Import reference model¶
This imports the reference model, sets up boundary conditions and computes the initial stresses prior to optimization.
# Material model (plane stress)
material = IsotropicElasticityPlaneStress(E=1000.0, nu=0.3)
# Import mesh
specimen = import_mesh(get_example_file("fillet.vtu"), material)
# Symmetry nodes
left = specimen.nodes[:, 0] < 0.1
specimen.constraints[left, 0] = True
bottom = specimen.nodes[:, 1] < 0.1
specimen.constraints[bottom, 1] = True
# Load at right end
right = specimen.nodes[:, 0] >= specimen.nodes[:, 0].max() - 0.1
specimen.constraints[right, 0] = True
specimen.displacements[right, 0] = 6.0
u, f, σ, ε, α = specimen.solve()
mises = torch.sqrt(σ[:, 0] ** 2 - σ[:, 0] * σ[:, 1] + σ[:, 1] ** 2 + 3 * σ[:, 2] ** 2)
specimen.plot(u, bcs=False, element_property=mises, cmap="inferno")
max_mises = torch.max(mises)
print(f"Maximum v.Mises stress is {max_mises:.2f}.")
Maximum v.Mises stress is 164.19.
Mesh morphing with radial basis functions¶
The morph
function applies a shape change to a region of the mesh. It uses radial basis functions to regularize the shape change.
# Radial basis function
r = torch.cdist(specimen.nodes, specimen.nodes)
epsilon = 0.2
phi = torch.exp(-((epsilon * r) ** 2))
def morph(fem, nids, x, dir):
# Build matrix
M = phi[:, nids]
M = M[nids, :]
# Solve linear equation system
weights = torch.linalg.solve(M, x - fem.nodes[nids, dir])
# Apply deformation
fem.nodes[:, dir] += weights @ phi[nids, :]
Control nodes¶
We use a set of control nodes as design variables, which may be used to modify the shape. These nodes are located along the fillet radius.
control_nodes = [3, 5, 7, 58]
# Plot the base mesh
specimen.plot(bcs=False)
# Plot control nodes
cx = specimen.nodes[control_nodes, 0]
cy = specimen.nodes[control_nodes, 1]
plt.scatter(cx, cy, marker="o", color="black")
for node in control_nodes:
plt.annotate(str(node), (specimen.nodes[node, 0] + 1, specimen.nodes[node, 1] + 1))
Shape optimization¶
The objective function $f$ uses a set of control nodes to morph the mesh and compute the resulting top N stresses. It returns the an aggregate of the v. Mises stresses, which should be minimized. The aggregate is computed with the Kreisselmeier–Steinhauser function $$ \sigma_{agg} = \frac{1}{\eta} \ln \sum_i^N \exp\left(\eta \bar{\sigma}_i\right) $$ with $$ \bar{\sigma}_i = \frac{\sigma_\text{mises}}{\sigma_\text{mises}^\text{0,max}} - 1, $$ where $\sigma_\text{mises}^\text{0,max}$ is the maximum v.Mises stress in the first iteration and $\eta$ is a regularization parameter.
This aggregate is chosen to regularize the problem. If only the maximum would be chosen, the responsible maximum stressed element might jump from iteration to iteration.
def f(x):
# Detach previous gradients
specimen.nodes = specimen.nodes.detach()
# Update nodes
morph(specimen, control_nodes, x, 1)
# Solve fem with updated nodes
_, _, σ, _, _ = specimen.solve()
# Compute stress
mises = torch.sqrt(
σ[:, 0] ** 2 - σ[:, 0] * σ[:, 1] + σ[:, 1] ** 2 + 3 * σ[:, 2] ** 2
)
# Compute Kreisselmeier–Steinhauser function to aggregate stresses
eta = 10.0
agg = 1 / eta * torch.log(torch.sum(torch.exp(eta * (mises / max_mises - 1))))
# Return average of top stresses
return agg
# Initial values (= current y node positions)
x = torch.tensor(specimen.nodes[control_nodes, 1].numpy(), requires_grad=True)
# Optimizer settings
optimizer = torch.optim.Adam([x], lr=0.04)
# Variable to store intermediate values
stresses = []
for _ in tqdm(range(100)):
optimizer.zero_grad()
objective = f(x)
stresses.append(objective.detach().item())
objective.backward()
optimizer.step()
0%| | 0/100 [00:00<?, ?it/s]
2%|▏ | 2/100 [00:00<00:05, 17.14it/s]
4%|▍ | 4/100 [00:00<00:05, 17.24it/s]
6%|▌ | 6/100 [00:00<00:05, 17.63it/s]
8%|▊ | 8/100 [00:00<00:05, 17.55it/s]
10%|█ | 10/100 [00:00<00:05, 17.42it/s]
12%|█▏ | 12/100 [00:00<00:05, 17.42it/s]
14%|█▍ | 14/100 [00:00<00:04, 17.39it/s]
16%|█▌ | 16/100 [00:00<00:04, 17.63it/s]
18%|█▊ | 18/100 [00:01<00:04, 17.75it/s]
20%|██ | 20/100 [00:01<00:04, 17.77it/s]
22%|██▏ | 22/100 [00:01<00:04, 17.76it/s]
24%|██▍ | 24/100 [00:01<00:04, 17.67it/s]
26%|██▌ | 26/100 [00:01<00:04, 17.70it/s]
28%|██▊ | 28/100 [00:01<00:04, 17.77it/s]
30%|███ | 30/100 [00:01<00:03, 17.87it/s]
32%|███▏ | 32/100 [00:01<00:03, 17.79it/s]
34%|███▍ | 34/100 [00:01<00:03, 17.88it/s]
36%|███▌ | 36/100 [00:02<00:03, 17.79it/s]
38%|███▊ | 38/100 [00:02<00:03, 17.78it/s]
40%|████ | 40/100 [00:02<00:03, 17.83it/s]
42%|████▏ | 42/100 [00:02<00:03, 17.73it/s]
44%|████▍ | 44/100 [00:02<00:03, 17.70it/s]
46%|████▌ | 46/100 [00:02<00:03, 17.84it/s]
48%|████▊ | 48/100 [00:02<00:02, 17.83it/s]
50%|█████ | 50/100 [00:02<00:02, 17.88it/s]
52%|█████▏ | 52/100 [00:02<00:02, 18.00it/s]
54%|█████▍ | 54/100 [00:03<00:02, 18.09it/s]
56%|█████▌ | 56/100 [00:03<00:02, 18.10it/s]
58%|█████▊ | 58/100 [00:03<00:02, 17.86it/s]
60%|██████ | 60/100 [00:03<00:02, 17.94it/s]
62%|██████▏ | 62/100 [00:03<00:02, 17.90it/s]
64%|██████▍ | 64/100 [00:03<00:02, 17.98it/s]
66%|██████▌ | 66/100 [00:03<00:01, 18.09it/s]
68%|██████▊ | 68/100 [00:03<00:01, 18.08it/s]
70%|███████ | 70/100 [00:03<00:01, 18.13it/s]
72%|███████▏ | 72/100 [00:04<00:01, 18.11it/s]
74%|███████▍ | 74/100 [00:04<00:01, 18.13it/s]
76%|███████▌ | 76/100 [00:04<00:01, 18.01it/s]
78%|███████▊ | 78/100 [00:04<00:01, 18.04it/s]
80%|████████ | 80/100 [00:04<00:01, 17.95it/s]
82%|████████▏ | 82/100 [00:04<00:01, 17.81it/s]
84%|████████▍ | 84/100 [00:04<00:00, 17.85it/s]
86%|████████▌ | 86/100 [00:04<00:00, 17.69it/s]
88%|████████▊ | 88/100 [00:04<00:00, 17.53it/s]
90%|█████████ | 90/100 [00:05<00:00, 17.54it/s]
92%|█████████▏| 92/100 [00:05<00:00, 17.65it/s]
94%|█████████▍| 94/100 [00:05<00:00, 17.71it/s]
96%|█████████▌| 96/100 [00:05<00:00, 17.62it/s]
98%|█████████▊| 98/100 [00:05<00:00, 17.49it/s]
100%|██████████| 100/100 [00:05<00:00, 17.63it/s]
100%|██████████| 100/100 [00:05<00:00, 17.78it/s]
plt.plot(stresses, ".-k")
plt.title("Optimization history")
plt.xlabel("Iteration")
plt.ylabel("Averaged top stress")
plt.show()
u, _, σ, _, _ = specimen.solve()
mises = torch.sqrt(σ[:, 0] ** 2 - σ[:, 0] * σ[:, 1] + σ[:, 1] ** 2 + 3 * σ[:, 2] ** 2)
specimen.plot(u, bcs=False, element_property=mises, figsize=(8, 8), cmap="inferno")
print(f"Maximum v.Mises stress is {torch.max(mises):.2f}.")
Maximum v.Mises stress is 136.09.