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, σ, F, α = specimen.solve()
mises = torch.sqrt(
σ[:, 0, 0] ** 2 - σ[:, 0, 0] * σ[:, 1, 1] + σ[:, 1, 1] ** 2 + 3 * σ[:, 0, 1] ** 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, 0] ** 2
- σ[:, 0, 0] * σ[:, 1, 1]
+ σ[:, 1, 1] ** 2
+ 3 * σ[:, 0, 1] ** 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]
3%|▎ | 3/100 [00:00<00:03, 25.99it/s]
6%|▌ | 6/100 [00:00<00:03, 26.55it/s]
9%|▉ | 9/100 [00:00<00:03, 26.34it/s]
12%|█▏ | 12/100 [00:00<00:03, 26.17it/s]
15%|█▌ | 15/100 [00:00<00:03, 26.40it/s]
18%|█▊ | 18/100 [00:00<00:03, 26.51it/s]
21%|██ | 21/100 [00:00<00:02, 26.50it/s]
24%|██▍ | 24/100 [00:00<00:02, 26.60it/s]
27%|██▋ | 27/100 [00:01<00:02, 26.68it/s]
30%|███ | 30/100 [00:01<00:02, 26.77it/s]
33%|███▎ | 33/100 [00:01<00:02, 26.85it/s]
36%|███▌ | 36/100 [00:01<00:02, 26.87it/s]
39%|███▉ | 39/100 [00:01<00:02, 26.90it/s]
42%|████▏ | 42/100 [00:01<00:02, 26.90it/s]
45%|████▌ | 45/100 [00:01<00:02, 26.91it/s]
48%|████▊ | 48/100 [00:01<00:01, 26.86it/s]
51%|█████ | 51/100 [00:01<00:01, 26.87it/s]
54%|█████▍ | 54/100 [00:02<00:01, 26.90it/s]
57%|█████▋ | 57/100 [00:02<00:01, 26.86it/s]
60%|██████ | 60/100 [00:02<00:01, 26.75it/s]
63%|██████▎ | 63/100 [00:02<00:01, 26.82it/s]
66%|██████▌ | 66/100 [00:02<00:01, 26.87it/s]
69%|██████▉ | 69/100 [00:02<00:01, 26.91it/s]
72%|███████▏ | 72/100 [00:02<00:01, 26.77it/s]
75%|███████▌ | 75/100 [00:02<00:00, 26.81it/s]
78%|███████▊ | 78/100 [00:02<00:00, 26.79it/s]
81%|████████ | 81/100 [00:03<00:00, 26.87it/s]
84%|████████▍ | 84/100 [00:03<00:00, 26.87it/s]
87%|████████▋ | 87/100 [00:03<00:00, 26.91it/s]
90%|█████████ | 90/100 [00:03<00:00, 26.95it/s]
93%|█████████▎| 93/100 [00:03<00:00, 27.01it/s]
96%|█████████▌| 96/100 [00:03<00:00, 27.03it/s]
99%|█████████▉| 99/100 [00:03<00:00, 27.01it/s]
100%|██████████| 100/100 [00:03<00:00, 26.79it/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, 0] ** 2 - σ[:, 0, 0] * σ[:, 1, 1] + σ[:, 1, 1] ** 2 + 3 * σ[:, 0, 1] ** 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.