Concurrent optimization of orientation and topology¶
import matplotlib.pyplot as plt
import numpy as np
import torch
from scipy.optimize import bisect
from tqdm import tqdm
torch.set_default_dtype(torch.double)
from torchfem import Planar
from torchfem.rotations import planar_rotation
Parameters¶
# Orthotropic material parameters
E_1 = 100000.0
E_2 = 10000.0
nu_12 = 0.3
G_12 = 5000.0
# Mesh parameters
Nx = 60
Ny = 30
# Optimization parameters (SIMP penalization, volume fraction, move limit, step size)
p = 3
volfrac = 0.4
move = 0.1
ori_step = 1000.0
# Volume constraint
V_0 = volfrac * Nx * Ny
Material model¶
The material is a linear, elastic, homogeneous and orthotropic material. If we assume a plane stress state, we may write the relation between stress and strain (material model) in terms of the compliance tensor $$ \begin{pmatrix} \varepsilon_{xx}\\ \varepsilon_{yy}\\ 2\varepsilon_{xy} \end{pmatrix} = \begin{pmatrix} \frac{1}{E_{xx}} & -\frac{\nu_{xy}}{E_{xx}} & 0\\ -\frac{\nu_{yx}}{E_{yy}} & \frac{1}{E_{yy}} & 0\\ 0 & 0 & \frac{1}{G_{xy}} \end{pmatrix} \begin{pmatrix} \sigma_{xx}\\ \sigma_{yy}\\ \sigma_{xy} \end{pmatrix} $$ or as stiffness tensor $$ \begin{pmatrix} \sigma_{xx}\\ \sigma_{yy}\\ \sigma_{xy} \end{pmatrix} = \underbrace{\begin{pmatrix} \frac{E_{xx}}{1-\nu_{xy}\nu_{yx}} & \frac{\nu_{xy}E_{yy}}{1-\nu_{xy}\nu_{yx}} & 0\\ \frac{\nu_{yx}E_{xx}}{1-\nu_{xy}\nu_{yx}}& \frac{E_{yy}}{1-\nu_{xy}\nu_{yx}} & 0\\ 0 & 0 & G_{xy} \end{pmatrix}}_{\bm{C}} \begin{pmatrix} \varepsilon_{xx}\\ \varepsilon_{yy}\\ 2\varepsilon_{xy} \end{pmatrix} $$
The stiffness is characterized by four properties $E_{xx}, E_{yy}, G_{xy}, \nu_{xy}$. The tensor is symmetric with $$ \frac{\nu_{xy}}{E_{xx}}=\frac{\nu_{yx}}{E_{yy}} $$
Fortunately, we have a 'torchfem' function that computes the stiffness tensor from the four properties.
from torchfem.materials import OrthotropicElasticityPlaneStress
mat = OrthotropicElasticityPlaneStress(E_1, E_2, nu_12, G_12)
Mesh generation¶
We create a simple domain of $N_x$ by $N_y$ elements. The domain is a rectangle and we apply a singular force at the tip, while the left side is fully clamped.
# Create nodes
n1 = torch.linspace(0.0, Nx, Nx + 1)
n2 = torch.linspace(0.0, Ny, Ny + 1)
n1, n2 = torch.stack(torch.meshgrid(n1, n2, indexing="ij"))
nodes = torch.stack([n1.ravel(), n2.ravel()], dim=1)
# Create elements connecting nodes
elements = []
for i in range(Nx):
for j in range(Ny):
n0 = j + i * (Ny + 1)
elements.append([n0, n0 + Ny + 1, n0 + Ny + 2, n0 + 1])
elements = torch.tensor(elements)
# Create the beam model
beam = Planar(nodes, elements, mat)
# Load at top
beam.forces[(Nx + 1) * (Ny + 1) - Ny - 1, 1] = -1
# Constrained displacement at left end
beam.constraints[nodes[:, 0] == 0, :] = True
# Create and plot the domain
beam.plot()
Objective function¶
The objective function is the compliance of the structure. The compliance is defined as $$ C = \frac{1}{2}\bm{u}^\top \bm{f} $$ where $\bm{u}$ is the global displacement vector and $\bm{f}$ is the global force vector.
The compliance is a measure of the energy stored in the structure due to the applied force and we want to minimize w.r.t. to the relative element densities $\bm{\rho}$ and the element-wise fiber orientations $\bm{\phi}$ in order to get a stiff structure.
def compliance(rho, phi):
# Rotate
R = planar_rotation(phi)
beam.material = mat.vectorize(beam.n_elem).rotate(R)
# Apply density
beam.material.C = rho[:, None, None] ** p * beam.material.C
u, f, _, _, _ = beam.solve()
return 0.5 * torch.inner(u.ravel(), f.ravel())
Filter¶
We employ a sensitivity filter for the relative densities to regularize the problem (mesh dependency) and to avoid checkerboard patterns. The filter is defined as $$ \widetilde{\frac{\partial C}{\partial \rho_j}} = \frac{1}{\rho_j} \frac{\sum_i H_{ji} \rho_i \frac{\partial C}{\partial \rho_i} }{\sum_i H_{ji}} $$ where $H_{ji}$ is the filter kernel. We use a simple linear filter kernel defined as $$ H_{ji} = \max(0, r - \lVert \bm{x}_i - \bm{x}_j \rVert) $$ with element centers $\bm{x}_i$ and $\bm{x}_j$ and filter radius $r$.
filter_radius = 1.5
ecenters = torch.stack([torch.mean(nodes[e], dim=0) for e in elements])
dist = torch.cdist(ecenters, ecenters)
H = filter_radius - dist
H[dist > filter_radius] = 0.0
The optimizer¶
def optimize(rho, phi, n_iter=100):
# Bounds
rho_min = 0.001 * torch.ones_like(rho)
rho_max = torch.ones_like(rho)
# Storage for compliance
energies = []
for _ in tqdm(range(n_iter)):
C = compliance(rho, phi)
dC_dphi = torch.autograd.grad(C, phi, retain_graph=True)[0]
dC_drho = torch.autograd.grad(C, rho)[0]
dC_drho = H @ (rho * dC_drho) / H.sum(dim=0) / rho
# For a certain value of mu, apply the iteration scheme
def make_step(mu):
G_k = -dC_drho / mu
upper = torch.min(rho_max, (1 + move) * rho)
lower = torch.max(rho_min, (1 - move) * rho)
rho_trial = G_k**0.5 * rho
return torch.max(torch.min(rho_trial, upper), lower)
# Constraint function
def g(mu):
rho_k = make_step(mu)
return rho_k.sum() - V_0
# Find the root of g(mu)
with torch.no_grad():
mu = bisect(g, 1e-10, 100.0)
# Advance rho and phi
rho.data = make_step(mu)
phi.data -= ori_step * dC_dphi.data
# Track compliance
with torch.no_grad():
energies.append(C.item())
return energies
@torch.no_grad()
def plot_solution(rho, phi, ptype="element", color="lightskyblue"):
# Compute properties
centers = beam.nodes[beam.elements, :].mean(dim=1)
dir = torch.stack([torch.cos(phi), -torch.sin(phi), torch.zeros_like(phi)]).T
# Plot orientations
beam.plot(element_property=rho, linewidth=0.2, cmap="gray_r")
if ptype == "element":
plt.quiver(
centers[:, 0],
centers[:, 1],
dir[:, 0],
dir[:, 1],
pivot="middle",
headlength=0,
headaxislength=0,
headwidth=0,
width=0.003,
color=color,
alpha=rho,
)
if ptype == "stream":
dir[rho < 0.75] = 0.0
plt.streamplot(
centers[:, 0].reshape(Nx, Ny).numpy()[:, 0],
centers[:, 1].reshape(Nx, Ny).numpy()[0, :],
dir[:, 0].reshape(Nx, Ny).T.numpy(),
dir[:, 1].reshape(Nx, Ny).T.numpy(),
color=color,
broken_streamlines=False,
arrowstyle="-",
cmap="coolwarm",
)
plt.show()
Optimize with fibers initially aligned in x¶
# Design variables
phi = 0.0 * torch.ones((len(beam.elements)), requires_grad=True)
rho = volfrac * torch.ones((len(beam.elements)), requires_grad=True)
energies = optimize(rho, phi, 200)
plt.semilogy(energies, ".-k")
plt.grid()
plt.title("Optimization history")
plt.xlabel("Iteration")
plt.ylabel("Compliance")
plt.show()
0%| | 0/200 [00:00<?, ?it/s]
0%| | 1/200 [00:00<00:38, 5.12it/s]
1%| | 2/200 [00:00<00:37, 5.23it/s]
2%|▏ | 3/200 [00:00<00:37, 5.22it/s]
2%|▏ | 4/200 [00:00<00:36, 5.31it/s]
2%|▎ | 5/200 [00:00<00:36, 5.31it/s]
3%|▎ | 6/200 [00:01<00:36, 5.29it/s]
4%|▎ | 7/200 [00:01<00:36, 5.26it/s]
4%|▍ | 8/200 [00:01<00:36, 5.22it/s]
4%|▍ | 9/200 [00:01<00:36, 5.18it/s]
5%|▌ | 10/200 [00:01<00:37, 5.12it/s]
6%|▌ | 11/200 [00:02<00:37, 5.02it/s]
6%|▌ | 12/200 [00:02<00:40, 4.70it/s]
6%|▋ | 13/200 [00:02<00:40, 4.58it/s]
7%|▋ | 14/200 [00:02<00:40, 4.54it/s]
8%|▊ | 15/200 [00:03<00:44, 4.12it/s]
8%|▊ | 16/200 [00:03<00:47, 3.84it/s]
8%|▊ | 17/200 [00:03<00:48, 3.77it/s]
9%|▉ | 18/200 [00:03<00:48, 3.78it/s]
10%|▉ | 19/200 [00:04<00:49, 3.65it/s]
10%|█ | 20/200 [00:04<00:47, 3.83it/s]
10%|█ | 21/200 [00:04<00:49, 3.65it/s]
11%|█ | 22/200 [00:05<00:48, 3.70it/s]
12%|█▏ | 23/200 [00:05<00:45, 3.85it/s]
12%|█▏ | 24/200 [00:05<00:43, 4.02it/s]
12%|█▎ | 25/200 [00:05<00:47, 3.69it/s]
13%|█▎ | 26/200 [00:06<00:52, 3.34it/s]
14%|█▎ | 27/200 [00:06<00:49, 3.50it/s]
14%|█▍ | 28/200 [00:06<00:51, 3.33it/s]
14%|█▍ | 29/200 [00:07<00:49, 3.45it/s]
15%|█▌ | 30/200 [00:07<00:52, 3.24it/s]
16%|█▌ | 31/200 [00:07<00:52, 3.21it/s]
16%|█▌ | 32/200 [00:08<00:53, 3.15it/s]
16%|█▋ | 33/200 [00:08<00:55, 3.00it/s]
17%|█▋ | 34/200 [00:08<00:55, 2.99it/s]
18%|█▊ | 35/200 [00:09<00:55, 2.97it/s]
18%|█▊ | 36/200 [00:09<00:54, 3.00it/s]
18%|█▊ | 37/200 [00:09<00:54, 2.99it/s]
19%|█▉ | 38/200 [00:10<00:53, 3.05it/s]
20%|█▉ | 39/200 [00:10<00:53, 2.99it/s]
20%|██ | 40/200 [00:10<00:54, 2.94it/s]
20%|██ | 41/200 [00:11<00:53, 2.98it/s]
21%|██ | 42/200 [00:11<00:53, 2.97it/s]
22%|██▏ | 43/200 [00:11<00:54, 2.88it/s]
22%|██▏ | 44/200 [00:12<00:54, 2.85it/s]
22%|██▎ | 45/200 [00:12<00:53, 2.88it/s]
23%|██▎ | 46/200 [00:12<00:54, 2.84it/s]
24%|██▎ | 47/200 [00:13<00:53, 2.85it/s]
24%|██▍ | 48/200 [00:13<00:53, 2.82it/s]
24%|██▍ | 49/200 [00:13<00:54, 2.79it/s]
25%|██▌ | 50/200 [00:14<00:53, 2.81it/s]
26%|██▌ | 51/200 [00:14<00:53, 2.80it/s]
26%|██▌ | 52/200 [00:15<00:52, 2.81it/s]
26%|██▋ | 53/200 [00:15<00:52, 2.81it/s]
27%|██▋ | 54/200 [00:15<00:51, 2.82it/s]
28%|██▊ | 55/200 [00:16<00:52, 2.79it/s]
28%|██▊ | 56/200 [00:16<00:51, 2.80it/s]
28%|██▊ | 57/200 [00:16<00:51, 2.80it/s]
29%|██▉ | 58/200 [00:17<00:50, 2.80it/s]
30%|██▉ | 59/200 [00:17<00:50, 2.80it/s]
30%|███ | 60/200 [00:17<00:49, 2.80it/s]
30%|███ | 61/200 [00:18<00:49, 2.81it/s]
31%|███ | 62/200 [00:18<00:49, 2.79it/s]
32%|███▏ | 63/200 [00:18<00:49, 2.77it/s]
32%|███▏ | 64/200 [00:19<00:51, 2.66it/s]
32%|███▎ | 65/200 [00:19<00:52, 2.55it/s]
33%|███▎ | 66/200 [00:20<00:51, 2.61it/s]
34%|███▎ | 67/200 [00:20<00:50, 2.66it/s]
34%|███▍ | 68/200 [00:20<00:48, 2.71it/s]
34%|███▍ | 69/200 [00:21<00:48, 2.73it/s]
35%|███▌ | 70/200 [00:21<00:47, 2.76it/s]
36%|███▌ | 71/200 [00:21<00:46, 2.79it/s]
36%|███▌ | 72/200 [00:22<00:45, 2.81it/s]
36%|███▋ | 73/200 [00:22<00:44, 2.82it/s]
37%|███▋ | 74/200 [00:22<00:44, 2.82it/s]
38%|███▊ | 75/200 [00:23<00:44, 2.82it/s]
38%|███▊ | 76/200 [00:23<00:43, 2.83it/s]
38%|███▊ | 77/200 [00:24<00:43, 2.85it/s]
39%|███▉ | 78/200 [00:24<00:44, 2.75it/s]
40%|███▉ | 79/200 [00:24<00:44, 2.74it/s]
40%|████ | 80/200 [00:25<00:43, 2.78it/s]
40%|████ | 81/200 [00:25<00:42, 2.80it/s]
41%|████ | 82/200 [00:25<00:42, 2.77it/s]
42%|████▏ | 83/200 [00:26<00:42, 2.77it/s]
42%|████▏ | 84/200 [00:26<00:42, 2.75it/s]
42%|████▎ | 85/200 [00:26<00:41, 2.74it/s]
43%|████▎ | 86/200 [00:27<00:40, 2.79it/s]
44%|████▎ | 87/200 [00:27<00:40, 2.82it/s]
44%|████▍ | 88/200 [00:28<00:39, 2.83it/s]
44%|████▍ | 89/200 [00:28<00:39, 2.84it/s]
45%|████▌ | 90/200 [00:28<00:38, 2.84it/s]
46%|████▌ | 91/200 [00:29<00:38, 2.84it/s]
46%|████▌ | 92/200 [00:29<00:38, 2.81it/s]
46%|████▋ | 93/200 [00:29<00:38, 2.81it/s]
47%|████▋ | 94/200 [00:30<00:37, 2.82it/s]
48%|████▊ | 95/200 [00:30<00:37, 2.78it/s]
48%|████▊ | 96/200 [00:30<00:37, 2.78it/s]
48%|████▊ | 97/200 [00:31<00:36, 2.81it/s]
49%|████▉ | 98/200 [00:31<00:36, 2.82it/s]
50%|████▉ | 99/200 [00:31<00:36, 2.80it/s]
50%|█████ | 100/200 [00:32<00:35, 2.81it/s]
50%|█████ | 101/200 [00:32<00:35, 2.82it/s]
51%|█████ | 102/200 [00:32<00:34, 2.85it/s]
52%|█████▏ | 103/200 [00:33<00:33, 2.86it/s]
52%|█████▏ | 104/200 [00:33<00:33, 2.85it/s]
52%|█████▎ | 105/200 [00:34<00:33, 2.83it/s]
53%|█████▎ | 106/200 [00:34<00:33, 2.84it/s]
54%|█████▎ | 107/200 [00:34<00:32, 2.85it/s]
54%|█████▍ | 108/200 [00:35<00:32, 2.85it/s]
55%|█████▍ | 109/200 [00:35<00:31, 2.86it/s]
55%|█████▌ | 110/200 [00:35<00:31, 2.86it/s]
56%|█████▌ | 111/200 [00:36<00:31, 2.84it/s]
56%|█████▌ | 112/200 [00:36<00:31, 2.84it/s]
56%|█████▋ | 113/200 [00:36<00:30, 2.84it/s]
57%|█████▋ | 114/200 [00:37<00:30, 2.85it/s]
57%|█████▊ | 115/200 [00:37<00:29, 2.84it/s]
58%|█████▊ | 116/200 [00:37<00:29, 2.86it/s]
58%|█████▊ | 117/200 [00:38<00:28, 2.87it/s]
59%|█████▉ | 118/200 [00:38<00:28, 2.85it/s]
60%|█████▉ | 119/200 [00:38<00:28, 2.86it/s]
60%|██████ | 120/200 [00:39<00:28, 2.84it/s]
60%|██████ | 121/200 [00:39<00:27, 2.88it/s]
61%|██████ | 122/200 [00:39<00:27, 2.87it/s]
62%|██████▏ | 123/200 [00:40<00:26, 2.86it/s]
62%|██████▏ | 124/200 [00:40<00:26, 2.89it/s]
62%|██████▎ | 125/200 [00:41<00:27, 2.77it/s]
63%|██████▎ | 126/200 [00:41<00:26, 2.75it/s]
64%|██████▎ | 127/200 [00:41<00:26, 2.80it/s]
64%|██████▍ | 128/200 [00:42<00:25, 2.80it/s]
64%|██████▍ | 129/200 [00:42<00:25, 2.81it/s]
65%|██████▌ | 130/200 [00:42<00:24, 2.81it/s]
66%|██████▌ | 131/200 [00:43<00:24, 2.83it/s]
66%|██████▌ | 132/200 [00:43<00:23, 2.84it/s]
66%|██████▋ | 133/200 [00:43<00:23, 2.86it/s]
67%|██████▋ | 134/200 [00:44<00:23, 2.85it/s]
68%|██████▊ | 135/200 [00:44<00:22, 2.87it/s]
68%|██████▊ | 136/200 [00:44<00:22, 2.89it/s]
68%|██████▊ | 137/200 [00:45<00:22, 2.78it/s]
69%|██████▉ | 138/200 [00:45<00:21, 2.83it/s]
70%|██████▉ | 139/200 [00:46<00:21, 2.83it/s]
70%|███████ | 140/200 [00:46<00:21, 2.79it/s]
70%|███████ | 141/200 [00:46<00:21, 2.79it/s]
71%|███████ | 142/200 [00:47<00:20, 2.83it/s]
72%|███████▏ | 143/200 [00:47<00:20, 2.82it/s]
72%|███████▏ | 144/200 [00:47<00:19, 2.86it/s]
72%|███████▎ | 145/200 [00:48<00:19, 2.84it/s]
73%|███████▎ | 146/200 [00:48<00:19, 2.83it/s]
74%|███████▎ | 147/200 [00:48<00:18, 2.84it/s]
74%|███████▍ | 148/200 [00:49<00:18, 2.84it/s]
74%|███████▍ | 149/200 [00:49<00:17, 2.88it/s]
75%|███████▌ | 150/200 [00:49<00:17, 2.91it/s]
76%|███████▌ | 151/200 [00:50<00:16, 2.91it/s]
76%|███████▌ | 152/200 [00:50<00:16, 2.92it/s]
76%|███████▋ | 153/200 [00:50<00:16, 2.87it/s]
77%|███████▋ | 154/200 [00:51<00:15, 2.89it/s]
78%|███████▊ | 155/200 [00:51<00:15, 2.86it/s]
78%|███████▊ | 156/200 [00:51<00:15, 2.86it/s]
78%|███████▊ | 157/200 [00:52<00:15, 2.86it/s]
79%|███████▉ | 158/200 [00:52<00:14, 2.87it/s]
80%|███████▉ | 159/200 [00:52<00:14, 2.88it/s]
80%|████████ | 160/200 [00:53<00:13, 2.89it/s]
80%|████████ | 161/200 [00:53<00:13, 2.87it/s]
81%|████████ | 162/200 [00:54<00:13, 2.88it/s]
82%|████████▏ | 163/200 [00:54<00:12, 2.87it/s]
82%|████████▏ | 164/200 [00:54<00:12, 2.89it/s]
82%|████████▎ | 165/200 [00:55<00:12, 2.88it/s]
83%|████████▎ | 166/200 [00:55<00:11, 2.90it/s]
84%|████████▎ | 167/200 [00:55<00:11, 2.89it/s]
84%|████████▍ | 168/200 [00:56<00:10, 2.92it/s]
84%|████████▍ | 169/200 [00:56<00:10, 2.92it/s]
85%|████████▌ | 170/200 [00:56<00:10, 2.90it/s]
86%|████████▌ | 171/200 [00:57<00:10, 2.89it/s]
86%|████████▌ | 172/200 [00:57<00:09, 2.93it/s]
86%|████████▋ | 173/200 [00:57<00:09, 2.93it/s]
87%|████████▋ | 174/200 [00:58<00:08, 2.92it/s]
88%|████████▊ | 175/200 [00:58<00:08, 2.90it/s]
88%|████████▊ | 176/200 [00:58<00:08, 2.91it/s]
88%|████████▊ | 177/200 [00:59<00:07, 2.91it/s]
89%|████████▉ | 178/200 [00:59<00:07, 2.89it/s]
90%|████████▉ | 179/200 [00:59<00:07, 2.93it/s]
90%|█████████ | 180/200 [01:00<00:06, 2.92it/s]
90%|█████████ | 181/200 [01:00<00:06, 2.90it/s]
91%|█████████ | 182/200 [01:00<00:06, 2.91it/s]
92%|█████████▏| 183/200 [01:01<00:05, 2.89it/s]
92%|█████████▏| 184/200 [01:01<00:05, 2.90it/s]
92%|█████████▎| 185/200 [01:01<00:05, 2.86it/s]
93%|█████████▎| 186/200 [01:02<00:04, 2.93it/s]
94%|█████████▎| 187/200 [01:02<00:04, 2.92it/s]
94%|█████████▍| 188/200 [01:02<00:04, 2.89it/s]
94%|█████████▍| 189/200 [01:03<00:03, 2.88it/s]
95%|█████████▌| 190/200 [01:03<00:03, 2.89it/s]
96%|█████████▌| 191/200 [01:04<00:03, 2.90it/s]
96%|█████████▌| 192/200 [01:04<00:02, 2.88it/s]
96%|█████████▋| 193/200 [01:04<00:02, 2.90it/s]
97%|█████████▋| 194/200 [01:05<00:02, 2.88it/s]
98%|█████████▊| 195/200 [01:05<00:01, 2.90it/s]
98%|█████████▊| 196/200 [01:05<00:01, 2.89it/s]
98%|█████████▊| 197/200 [01:06<00:01, 2.86it/s]
99%|█████████▉| 198/200 [01:06<00:00, 2.88it/s]
100%|█████████▉| 199/200 [01:06<00:00, 2.91it/s]
100%|██████████| 200/200 [01:07<00:00, 2.90it/s]
100%|██████████| 200/200 [01:07<00:00, 2.98it/s]
plot_solution(rho, phi)
Generate fiber paths with streamlines¶
u, f, σ, _, _ = beam.solve()
e = torch.stack(
[torch.linalg.eigvalsh(torch.tensor([[s[0], s[2]], [s[2], s[1]]])) for s in σ]
)
mag, pos = torch.max(torch.abs(e), dim=1)
pval = torch.tensor([ev[p] for ev, p in zip(e, pos)])
pval = pval.reshape(Nx, Ny).T.numpy()
plot_solution(rho, phi, ptype="stream", color=pval)