3D Topology Optimization Example
================================
Classic 3D Structured Meshes
-----------------------------
In this example we will show how you can use pyFANTOM to perform minimum compliance TO on **structured meshes** in 3D. For this example we will perform TO on the bridge problem.
Below we detail how you can do this in a few lines of code!
=============================
Importing The Necessary Tools
=============================
First we import the tools from pyFANTOM we will be using in this example.
.. code-block:: python
from pyFANTOM.CPU import(
StructuredMesh3D, # Mesh Object for 3D Structured Meshes
StructuredFilter3D, # Filter kernel for 3D Structured Meshes
StructuredStiffnessKernel, # Stiffness Kernel for 3D Structured Meshes
FiniteElement, # Finite Element Object to Setup Boundary Conditions and RHS
CHOLMOD, CG, GMRES, MultiGrid, SPLU, SPSOLVE, # Sparse Solvers
MinimumCompliance, # TO Problem Definition
PGD, MMA, OC # Nonlinear Optimizers
)
from pyFANTOM.Physics import LinearElasticity # Physics Model For FEA
# Other Imports
import numpy as np
import matplotlib.pyplot as plt
from tqdm import trange
===============================
Setting Up Physics and Geometry
===============================
The first step in using pyFANTOM is defining the domain of the problem and setting up the physics for FEA.
.. code-block:: python
length = 1.0
height = 0.25
width = 0.25
# Create a physics model
physics = LinearElasticity(E=1.0, nu=1/3, thickness=1.0, type='PlaneStress')
# Create a mesh
mesh = StructuredMesh3D(nx=64, ny=32, nz=32, lx=length, ly=height, lz=width, physics=physics)
====================================
Setting Up The Finite Element Solver
====================================
The next step in using pyFANTOM is setting up a stiffness kernel and finite-element boundary conditions.
.. code-block:: python
# Create a stiffness kernel
stiffness_kernel = StructuredStiffnessKernel(mesh=mesh)
# Create a solver
# This can be any of the sparse solvers in pyFANTOM but in 3D CHOLMOD quickly becomes infeasible
solver = MultiGrid(mesh=mesh, kernel=stiffness_kernel, n_level=4, cycle='W', w_level=1, maxiter=200, tol=1e-4)
# Create a finite-element object
fe = FiniteElement(mesh=mesh, kernel=stiffness_kernel, solver=solver)
# Visualize the problem
fe.visualize_problem()
.. raw:: html
As seen above we do not have loads or boundary conditions. So now we will set this up for the bridge problem:
.. code-block:: python
left_edge_nodes = np.where(mesh.nodes[:, 0] < 1e-12)[0]
right_edge_nodes = np.where(mesh.nodes[:, 0] > length - 1e-12)[0]
bottom_edge_nodes = np.where(mesh.nodes[:, 1] < 1e-12)[0]
fe.reset_dirichlet_boundary_conditions()
fe.reset_forces()
# Apply force to the bottom edge
fe.add_point_forces(
node_ids=bottom_edge_nodes,
positions=None,
forces=np.array([[0.0, -1.0/len(bottom_edge_nodes), 0.0]])
)
# Apply zero displacement to the left edge and the right edge
fe.add_dirichlet_boundary_condition(
node_ids=np.concatenate([left_edge_nodes, right_edge_nodes]), # Node IDs to apply the boundary condition to
positions=None, # Alternative way to specify the boundary condition is to pass positions in space
dofs=np.array([[1, 1, 1]]), # Degrees of freedom to apply the boundary condition to
rhs=0.0 # Value to apply the boundary condition to
)
# Visualize the problem
fe.visualize_problem()
.. raw:: html
====================================
Setting Up TO Problem And Optimizer
====================================
Once the finite element is setup we can define the problem and optimizer to perform TO.
.. code-block:: python
# Define the filter kernel for TO
filter_kernel = StructuredFilter3D(mesh=mesh, r_min=1.5)
# Define the TO problem
to_problem = MinimumCompliance(
FE=fe,
filter=filter_kernel,
E_mul=[1.0], # You can pass a list of values to perform multi-material TO
volume_fraction=[0.1], # You can pass a list of values to perform volume fraction control for each material
void=1e-9, # You can pass a value to set void modulus
penalty=3, # You can pass a value to set penalty factor
#penalty_schedule = lambda p, i: (p-1)*np.round(4 * min(100, i) / 100)/4 + 1, # You can pass a function to set penalty schedule here
heavyside= True, # You can pass a boolean to use heavyside or not
eta=0.5, # You can pass a value to set eta - only used for heavyside
beta=2.0, # You can pass a value to set beta, or a function of iteration to set beta schedule - only used for heavyside
)
# Define the optimizer
optimizer = PGD( # You can use any of the optimizers in pyFANTOM: OC, MMA, PGD
problem=to_problem,
change_tol=np.inf, # No change tolerance
fun_tol=1e-4, # Function tolerance (convergence criterion for the optimizer)
)
====================================
Running The Optimization Loop
====================================
Now we can run the optimization loop and perform TO.
.. code-block:: python
maximum_iterations = 300
Progressbar = trange(maximum_iterations, desc='Optimizer Iterations', leave=True)
objective_history = []
for i in Progressbar:
optimizer.iter()
Progressbar.set_postfix(
optimizer.logs()
)
objective_history.append(optimizer.logs()['objective'])
if optimizer.converged():
print(f'Converged in {i} iterations')
break
The optimization converged in 46 iterations with the following output:
::
Optimizer Iterations: 15%|█▌ | 46/300 [02:12<12:11, 2.88s/it, objective=132, variable change=2.34, function change=7.19e-5, iteration=48, residual=8.09e-5] Converged in 46 iterations
Now we can visualize the resulting topology:
.. code-block:: python
to_problem.visualize_solution()
.. raw:: html
Running on GPU
--------------
The exact same problem can be run on GPU by just switching the backend.
A few notes on CUDA back-ends:
- In general the only exact solver available for CUDA is SPSOLVE, but in general for structured meshes the Multi-Grid solver with our custom CUDA kernels is the fastest solver on GPU.
- The CUDA backend uses cupy, however, the inputs to the FE class can be numpy arrays.
.. code-block:: python
from pyFANTOM.CUDA import(
StructuredMesh3D, # Mesh Object for 3D Structured Meshes
StructuredFilter3D, # Filter kernel for 3D Structured Meshes
StructuredStiffnessKernel, # Stiffness Kernel for 3D Structured Meshes
FiniteElement, # Finite Element Object to Setup Boundary Conditions and RHS
CG, GMRES, MultiGrid, SPSOLVE, # Sparse Solvers
MinimumCompliance, # TO Problem Definition
PGD, MMA, OC # Nonlinear Optimizers
)
from pyFANTOM.Physics import LinearElasticity # Physics Model For FEA
# Other Imports
import numpy as np
import matplotlib.pyplot as plt
from tqdm import trange
length = 1.0
width = 0.25
height = 0.25
# Create a physics model
physics = LinearElasticity(E=1.0, nu=1/3, thickness=1.0, type='PlaneStress')
# Create a mesh
mesh = StructuredMesh3D(nx=64, ny=32, nz=32, lx=length, ly=height, lz=width, physics=physics)
# Create a stiffness kernel
stiffness_kernel = StructuredStiffnessKernel(mesh = mesh)
# Create a solver
solver = MultiGrid(
mesh=mesh,
kernel=stiffness_kernel,
maxiter=200,
tol=1e-4,
n_level=4,
cycle='W',
w_level=1
)
# Create a finite-element object
fe = FiniteElement(mesh=mesh, kernel=stiffness_kernel, solver=solver)
left_edge_nodes = np.where(mesh.nodes[:, 0] < 1e-12)[0]
right_edge_nodes = np.where(mesh.nodes[:, 0] > length - 1e-12)[0]
bottom_edge_nodes = np.where(mesh.nodes[:, 1] < 1e-12)[0]
fe.reset_dirichlet_boundary_conditions()
fe.reset_forces()
# Apply force to the bottom edge
fe.add_point_forces(
node_ids=bottom_edge_nodes,
positions=None,
forces=np.array([[0.0, -1.0/len(bottom_edge_nodes), 0.0]])
)
# Apply zero displacement to the left edge and the right edge
fe.add_dirichlet_boundary_condition(
node_ids=np.concatenate([left_edge_nodes, right_edge_nodes]), # Node IDs to apply the boundary condition to
positions=None, # Alternative way to specify the boundary condition is to pass positions in space
dofs=np.array([[1, 1, 1]]), # Degrees of freedom to apply the boundary condition to
rhs=0.0 # Value to apply the boundary condition to
)
# Define the filter kernel for TO
filter_kernel = StructuredFilter3D(mesh=mesh, r_min=1.5)
# Define the TO problem
to_problem = MinimumCompliance(
FE=fe,
filter=filter_kernel,
E_mul=[1.0], # You can pass a list of values to perform multi-material TO
volume_fraction=[0.1], # You can pass a list of values to perform volume fraction control for each material
void=1e-9, # You can pass a value to set void modulus
penalty=3, # You can pass a value to set penalty factor
#penalty_schedule = lambda p, i: (p-1)*np.round(4 * min(100, i) / 100)/4 + 1, # You can pass a function to set penalty schedule here
heavyside= True, # You can pass a boolean to use heavyside or not
eta=0.5, # You can pass a value to set eta - only used for heavyside
beta=2.0, # You can pass a value to set beta, or a function of iteration to set beta schedule - only used for heavyside
)
# Define the optimizer
optimizer = PGD( # You can use any of the optimizers in pyFANTOM: OC, MMA, PGD
problem=to_problem,
change_tol=np.inf, # No change tolerance
fun_tol=1e-4, # Function tolerance (convergence criterion for the optimizer)
)
# Visualize the problem
fe.visualize_problem()
.. raw:: html
.. code-block:: python
maximum_iterations = 300
Progressbar = trange(maximum_iterations, desc='Optimizer Iterations', leave=True)
objective_history = []
for i in Progressbar:
optimizer.iter()
Progressbar.set_postfix(
optimizer.logs()
)
objective_history.append(optimizer.logs()['objective'])
if optimizer.converged():
print(f'Converged in {i} iterations')
break
The GPU optimization converged in 59 iterations:
::
Optimizer Iterations: 20%|█▉ | 59/300 [00:09<00:38, 6.32it/s, objective=131, variable change=1.97, function change=6.37e-5, iteration=61, residual=9.35e-5] Converged in 59 iterations
.. code-block:: python
to_problem.visualize_solution()
.. raw:: html
Unstructured Meshes
-------------------
pyFANTOM also supports unstructured meshes for topology optimization. Below we will use pygmesh to mesh a 3D model and perform TO on it.
**NOTE:**
- This is a toy problem to represent the capabilities of the package not a real-world problem.
- The Multi-Grid solver which is the most efficient way to solve the FEA problem is only available for structured meshes. So you may want to consider voxelizing geometry in some cases.
- The code below uses GPU since solving these larger 3D problems on CPU can be slow even with all of the efficient kernels that FANTOM ships with.
.. code-block:: python
from pyFANTOM.CUDA import(
GeneralMesh, # Mesh Object for Unstructured Meshes
GeneralFilter, # Filter kernel for Unstructured Meshes
GeneralStiffnessKernel, UniformStiffnessKernel, # Stiffness Kernel for Unstructured Meshes
FiniteElement, # Finite Element Object to Setup Boundary Conditions and RHS
CG, GMRES, MultiGrid, SPSOLVE, # Sparse Solvers
MinimumCompliance, # TO Problem Definition
PGD, MMA, OC # Nonlinear Optimizers
)
from pyFANTOM.Physics import LinearElasticity # Physics Model For FEA
import pygmsh
# Other Imports
import numpy as np
import cupy as cp
import matplotlib.pyplot as plt
from tqdm import trange
=====================
Create A Pac-Man Mesh
=====================
Below we will make a toy problem mesh using pygmesh.
.. code-block:: python
# Create a geometry object
with pygmsh.occ.Geometry([
'-setnumber', 'Mesh.Algorithm', '8',
'-setnumber', 'Mesh.SubdivisionAlgorithm', '1',
'-setnumber', 'Mesh.RecombinationAlgorithm', '2',
'-setnumber', 'Mesh.RecombineAll', '0',
'-setnumber', 'Mesh.SaveWithoutOrphans', '1'
]) as geom:
# Load the STEP file
geom.import_shapes("PacMan.step")
geom.characteristic_length_max = 10
geom.characteristic_length_min = 10
mesh_uniform = geom.generate_mesh()
mesh_uniform = [mesh_uniform.points[:,0:3], mesh_uniform.cells[2].data.astype(int).tolist()]
# Create a physics model
physics = LinearElasticity(E=1.0, nu=1/3, thickness=1.0, type='PlaneStress')
# Create a mesh
mesh = GeneralMesh(np.array(mesh_uniform[0])/1000, np.array(mesh_uniform[1]), physics=physics)
# Create a stiffness kernel
if mesh.is_uniform:
stiffness_kernel = UniformStiffnessKernel(mesh=mesh)
else:
stiffness_kernel = GeneralStiffnessKernel(mesh=mesh)
# Create a solver
solver = CG(kernel=stiffness_kernel, maxiter=5000, tol=1e-4)
# Create a finite-element object
fe = FiniteElement(mesh=mesh, kernel=stiffness_kernel, solver=solver)
# Visualize the mesh
fe.visualize_problem()
.. raw:: html
Now we will setup boundary conditions for this toy problem:
.. code-block:: python
fe.reset_dirichlet_boundary_conditions()
fe.reset_forces()
# find nodes on the boundary of the packman circle
bc_nodes = np.isclose(np.linalg.norm(mesh.nodes[:,0:2].get() - np.array([0.5,0.5]), axis=1),0.5)
bc_nodes = np.logical_and(bc_nodes, mesh.nodes[:,0].get() < 0.25)
bc_nodes = np.where(bc_nodes)[0]
fe.add_dirichlet_boundary_condition(
node_ids=bc_nodes,
positions=None,
dofs=np.array([[1, 1, 1]]),
rhs=0.0
)
# apply load at the mouth of the packman
upper_mouth = np.logical_and(np.isclose(np.abs(np.dot(mesh.nodes.get(),np.array([-1,1, 0]))),0), mesh.nodes[:,0].get() > 0.5)
force_node = np.where(upper_mouth)[0]
force_nodes = force_node[np.isin(force_node, bc_nodes, invert=True)]
force = np.array([[1,-1, 0]])/force_nodes.shape[0]/4 # Broadcastable, If needed you can provide one for each point
fe.add_point_forces(
node_ids=force_nodes,
positions=None,
forces=force
)
lower_mouth = np.logical_and(np.isclose(np.abs(np.dot(mesh.nodes.get(),np.array([1,1, 0]))),1.0), mesh.nodes[:,0].get() > 0.5)
force_node = np.where(lower_mouth)[0]
force_nodes = force_node[np.isin(force_node, bc_nodes, invert=True)]
force = np.array([[1,1, 0]])/force_nodes.shape[0]/4 # Broadcastable, If needed you can provide one for each point
fe.add_point_forces(
node_ids=force_nodes,
positions=None,
forces=force
)
# Visualize the problem
fe.visualize_problem()
.. raw:: html
Now we can just setup the problem and optimizer like before:
.. code-block:: python
# Define the filter kernel for TO This may take a while to compute for a large unstructured mesh
filter_kernel = GeneralFilter(mesh=mesh, r_min=0.01) # In general cases r_min is mesh space, not scaled by mesh size be cassreful!
# Define the TO problem
to_problem = MinimumCompliance(
FE=fe,
filter=filter_kernel,
E_mul=[1.0], # You can pass a list of values to perform multi-material TO
volume_fraction=[0.05], # You can pass a list of values to perform volume fraction control for each material
void=1e-9, # You can pass a value to set void modulus
penalty=3, # You can pass a value to set penalty factor
#penalty_schedule = lambda p, i: (p-1)*np.round(4 * min(100, i) / 100)/4 + 1, # You can pass a function to set penalty schedule here
heavyside= True, # You can pass a boolean to use heavyside or not
eta=0.5, # You can pass a value to set eta - only used for heavyside
beta=2.0, # You can pass a value to set beta, or a function of iteration to set beta schedule - only used for heavyside
)
# Define the optimizer
optimizer = PGD( # You can use any of the optimizers in pyFANTOM: OC, MMA, PGD
problem=to_problem,
change_tol=np.inf, # No change tolerance
fun_tol=1e-4, # Function tolerance (convergence criterion for the optimizer)
)
.. code-block:: python
maximum_iterations = 300
Progressbar = trange(maximum_iterations, desc='Optimizer Iterations', leave=True)
objective_history = []
for i in Progressbar:
optimizer.iter()
Progressbar.set_postfix(
optimizer.logs()
)
objective_history.append(optimizer.logs()['objective'])
if optimizer.converged():
print(f'Converged in {i} iterations')
break
The unstructured mesh optimization converged in 148 iterations:
::
Optimizer Iterations: 49%|████▉ | 148/300 [03:25<03:31, 1.39s/it, objective=50.6, variable change=0.726, function change=9.5e-5, iteration=150, residual=0.00177] Converged in 148 iterations
.. code-block:: python
# Visualize the solution
to_problem.visualize_solution()
.. raw:: html
As you can see this geometry is a bit of a mess. For best results you should work with as uniform of a hex mesh as possible instead of this simple tetra mesh.