In [1]:
import matplotlib.pyplot as plt
import meshio
import numpy as np
import pyvista
import torch
from scipy.optimize import bisect
from scipy.spatial import cKDTree
from tqdm import tqdm
from torchfem import Solid
from torchfem.data import get_data
from torchfem.materials import IsotropicElasticity3D
torch.set_default_dtype(torch.float64)
In [2]:
# Material model (Ti-6Al-4V) in imperial units
material = IsotropicElasticity3D(E=16500.0, nu=0.342)
Mesh¶
The domain is meshed with linear tetrahedrons in gmsh and subdivided in seven geometrical domains.
In [3]:
mesh = meshio.read(get_data("ge_bracket.vtu"))
elements = torch.tensor(mesh.cells[0].data)
nodes = torch.tensor(mesh.points)
domain = torch.tensor(mesh.cell_data["gmsh:geometrical"][0])
model = Solid(nodes, elements, material)
cmap = plt.get_cmap("tab10", 7)
model.plot(element_property={"Domain": domain}, cmap=cmap)
EmbeddableWidget(value='<iframe srcdoc="<!doctype html>\n<html lang="en">\n <head>\n <meta chars…
In [4]:
# Constrain inner nodes at fixation holes
R = 6.0
for d in [1, 3, 4, 5]:
dom = torch.unique(elements[domain == d])
center = nodes[dom].mean(dim=0)
con = (nodes[dom, 0] - center[0]) ** 2 + (nodes[dom, 1] - center[1]) ** 2 < R**2
model.constraints[dom[con], :] = True
# Define load cases (this is not using multi-point constraints yet...)
dom = torch.unique(elements[(domain == 2) | (domain == 7)])
load_case_1 = torch.zeros_like(nodes)
load_case_1[dom, 2] = 8000 / len(dom)
load_case_2 = torch.zeros_like(nodes)
load_case_2[dom, 1] = -8500 / len(dom)
load_case_3 = torch.zeros_like(nodes)
load_case_3[dom, 1] = -9500 * np.sin(np.deg2rad(42)) / len(dom)
load_case_3[dom, 2] = 9500 * np.cos(np.deg2rad(42)) / len(dom)
load_case_4 = torch.zeros_like(nodes)
levers = nodes[dom] - torch.tensor([0.0, 0.0, 0.0])
load_case_4[dom, 1] = 5000 / levers[:, 0] / len(dom)
# Apply load case 1 for testing
model.forces = load_case_1
# Solve
u, f, σ, F, α = model.solve(rtol=0.01, verbose=True)
─── torch-fem · solve ──────────────────────────────────────────────────────────────────
model Solid · 214,524 elem · 127,644 dof · float64
machine Apple M1 Pro · 8 threads · 16 GB RAM
solver minres · iterative · AMG · scipy · cpu
newton rtol 1e-02 · atol 1e-06 · ≤10 it
────────────────────────────────────────────────────────────────────────────────────────
Increment Load factor Steps Iterations Residual Wall time
1 1 1 1 6.10e-06 4.31 s
────────────────────────────────────────────────────────────────────────────────────────
converged · 1 increment · 1 iteration · 4.32 s
In [5]:
# Plot
model.plot(node_property={"u": u})
EmbeddableWidget(value='<iframe srcdoc="<!doctype html>\n<html lang="en">\n <head>\n <meta chars…
Optimization parameters¶
We define the optimization parameters, i.e. the volume fraction, the penalization factor, the move limit, the filter radius, and the stiffness of void relative to solid material.
The stiffness is interpolated as
$$ \mathbf{C}(\rho) = \mathbf{C}_\text{min} + \rho^p (\mathbf{C}_\text{max} - \mathbf{C}_\text{min}) $$ with $$ \mathbf{C}_\text{min} = \text{soft} \cdot \mathbf{C}_\text{max} $$
Void keeps a fixed fraction of the solid stiffness, so the density is free to approach zero without the stiffness following it down.
In [6]:
volfrac = 0.15
p = 3
move = 0.2
R = 5.0
soft = 1e-3
In [7]:
# Design elements are only the ones in domain 6
design_elements = elements[domain == 6]
vols = model.integrate_field()[domain == 6]
In [8]:
# Initial, minimum, and maximum values of design variables.
rho_0 = volfrac * torch.ones(len(design_elements))
rho_min = 1e-3 * torch.ones_like(rho_0)
rho_max = torch.ones_like(rho_0)
# Volume fraction
V_0 = volfrac * vols.sum()
# Analytical gradient of the stiffness matrix
k0 = model.k0()[domain == 6].clone()
C0 = model.material.C[domain == 6].clone()
C_min = soft * C0
# Precompute filter weights using scipy's KDTree
ecenters = torch.mean(nodes[design_elements], dim=1)
tree = cKDTree(ecenters)
d = tree.sparse_distance_matrix(tree, R, output_type="coo_matrix")
# Convert to torch sparse tensor
H = torch.sparse_coo_tensor(
indices=torch.stack([torch.as_tensor(d.row), torch.as_tensor(d.col)]),
values=torch.as_tensor(d.data),
size=(len(design_elements), len(design_elements)),
)
H_sum = H.sum(dim=0).to_dense()
Optimization with optimality constraints.¶
This should take around 10 minutes to run...
In [9]:
rho = [rho_0]
history = []
# Iterate solutions
for k in tqdm(range(25)):
# Interpolate stiffness between void and solid
model.material.C[domain == 6] = C_min + torch.einsum(
"n,nijkl->nijkl", rho[k] ** p, C0 - C_min
)
sensitivity = torch.zeros_like(rho[k])
# Iterate over load cases
for lc in [load_case_1, load_case_2, load_case_3, load_case_4]:
# Apply load case
model.forces = lc
# Compute solution
u_k, f_k, _, _, _ = model.solve(rtol=0.01)
# Evaluation of compliance
compliance = torch.inner(f_k.ravel(), u_k.ravel())
# Compute analytical sensitivities
u_j = u_k[design_elements].reshape(len(design_elements), -1)
w_k = torch.einsum("...i, ...ij, ...j", u_j, k0, u_j)
sensitivity += -p * rho[k] ** (p - 1.0) * w_k
# Filter sensitivities (if r provided)
sensitivity = H @ (rho[k] * sensitivity / vols) / H_sum / (rho[k] / vols)
# For a certain value of mu, apply the iteration scheme
def make_step(mu):
G_k = -sensitivity / mu
upper = torch.min(rho_max, (1 + move) * rho[k])
lower = torch.max(rho_min, (1 - move) * rho[k])
rho_trial = G_k**0.5 * rho[k]
return torch.maximum(torch.minimum(rho_trial, upper), lower)
# Constraint function
def g(mu):
rho_k = make_step(mu)
return torch.inner(rho_k, vols) - V_0
# Find the root of g(mu)
mu = bisect(g, 1e-10, 100.0)
# Update design history
rho.append(make_step(mu))
history.append(compliance)
100%|██████████| 25/25 [10:18<00:00, 24.73s/it]
In [10]:
plt.plot(history)
plt.xlabel("Iteration")
plt.ylabel("Compliance")
plt.grid()
plt.xlim(0, len(history) - 1)
plt.ylim(bottom=0)
plt.show()
Result¶
In [11]:
# Non-design elements are solid
rho_final = 2 * torch.ones(len(elements))
rho_final[domain == 6] = rho[-1]
cmap = plt.get_cmap("tab10", 2)
plotter = pyvista.Plotter()
model.plot(plotter=plotter, opacity=0.1)
model.plot(
plotter=plotter,
element_property={"rho": rho_final},
clip=("rho", 0.5),
show_edges=False,
cmap=cmap,
)
plotter.show(jupyter_backend="html")
EmbeddableWidget(value='<iframe srcdoc="<!doctype html>\n<html lang="en">\n <head>\n <meta chars…