Beam optimization with MPhysΒΆ
Note
The script for this example can be found under the examples/beam/ directory.
This example demonstrates TACS structural optimization capabilities. The optimization is setup using TACS' MPhys module, which acts as a wrapper for OpenMDAO. The beam model that we will be using for this problem is a rectangular beam, cantilevered, with a shear load applied at the tip. The beam is discretized using 100 beam elements along it's span.
The optimization problem is as follows: Minimize the mass of the beam with respect to the thickness of each element, subject to a max stress constraint dictated by the materials yield stress.
By realizing that the stress at any spanwise cross-section in the beam as a function of the running moment can be found independently using:
An analytical solution for this problem can be shown to be:
First, we import required libraries, define the model bdf file, and define important problem constants:
import os
import matplotlib.pyplot as plt
import numpy as np
import openmdao.api as om
from mphys import Multipoint
from mphys.scenario_structural import ScenarioStructural
from tacs import elements, constitutive, functions
from tacs.mphys import TacsBuilder
# BDF file containing mesh
bdf_file = os.path.join(os.path.dirname(__file__), "beam_opt.bdf")
# Beam thickness (initial)
t = 0.05 # m
# Beam width
w = 0.05 # m
# Length of beam
L = 1.0
# Material properties
rho = 2500.0 # density kg/m^3
E = 70.0e9 # Young's modulus (Pa)
nu = 0.0 # Poisson's ratio
ys = 350e6 # yield stress
# Shear force applied at tip
V = 1e3
Next we define an elemCallBack()
function.
This is a user-defined callback function for setting up TACS elements and element design variables.
We use the IsoRectangleBeamConstitutive
class for the constitutive properties,
assign a design variable number for the thickness parameter, and return a Beam2
element class.
# Callback function used to setup TACS element objects and DVs
def element_callback(dvNum, compID, compDescript, elemDescripts, specialDVs, **kwargs):
# Setup (isotropic) property and constitutive objects
prop = constitutive.MaterialProperties(rho=rho, E=E, nu=nu, ys=ys)
# Set one thickness dv for every property group
con = constitutive.IsoRectangleBeamConstitutive(prop, t=t, w=w, tNum=dvNum)
# Defines local y/thickness direction for beam
refAxis = np.array([0.0, 1.0, 0.0])
transform = elements.BeamRefAxisTransform(refAxis)
# Pass back the appropriate tacs element object
elem = elements.Beam2(transform, con)
return elem
We define a problem_setup()
function.
This function is called each time a new MPhys Scenario is created.
This function sets up the StaticProblem
by adding fixed loads, modifying options, and adding eval functions.
Here we specify the beam mass (StructuralMass
) and aggregated failure (KSFailure
) as outputs for our analysis and add our 1 kN shear load.
def problem_setup(scenario_name, fea_assembler, problem):
"""
Helper function to add fixed forces and eval functions
to structural problems used in tacs builder
"""
# Add TACS Functions
problem.addFunction("mass", functions.StructuralMass)
problem.addFunction(
"ks_vmfailure", functions.KSFailure, safetyFactor=1.0, ksWeight=100.0
)
# Add forces to static problem
problem.addLoadToNodes(101, [0.0, V, 0.0, 0.0, 0.0, 0.0], nastranOrdering=True)
Here we define our Multipoint
(essentially an OpenMDAO Group
) which will contain our analysis Scenario
.
To do this, we instantiate the TacsBuilder
using the element_callback
and problem_setup
we defined above.
We create OpenMDAO Component
's to feed design variable and mesh inputs to the Scenario
component.
We use this builder to create an MPhys StructuralScenario
.
class BeamModel(Multipoint):
def setup(self):
# Initialize MPHYS builder for TACS
struct_builder = TacsBuilder(
mesh_file=bdf_file,
element_callback=element_callback,
problem_setup=problem_setup,
coupled=False,
write_solution=False,
)
struct_builder.initialize(self.comm)
dv_array = struct_builder.get_initial_dvs()
# Create independent variable component to control design variables for problem
dvs = self.add_subsystem("dvs", om.IndepVarComp(), promotes=["*"])
dvs.add_output("dv_struct", dv_array)
# Add mesh component
self.add_subsystem("mesh", struct_builder.get_mesh_coordinate_subsystem())
self.mphys_add_scenario(
"tip_shear", ScenarioStructural(struct_builder=struct_builder)
)
self.mphys_connect_scenario_coordinate_source("mesh", "tip_shear", "struct")
# Connect dv component to input of structural scenario
self.connect("dv_struct", "tip_shear.dv_struct")
At this point we setup the OpenMDAO Problem
class that we will use to perform our optimization.
We assign our BeamModel
to the problem class and set ScipyOptimizeDriver
.
We define our design variables, constraint, and objective.
Finally we run the problem driver to optimize the problem.
# Instantiate OpenMDAO problem
prob = om.Problem()
prob.model = BeamModel()
model = prob.model
# Declare design variables, objective, and constraint
model.add_design_var("dv_struct", lower=0.001, upper=0.1, scaler=100.0)
model.add_objective("tip_shear.mass", scaler=1.0)
model.add_constraint("tip_shear.ks_vmfailure", lower=0.0, upper=1.0, scaler=1.0)
# Configure optimizer
prob.driver = om.ScipyOptimizeDriver(debug_print=["objs", "nl_cons"], maxiter=1000)
prob.driver.options["optimizer"] = "SLSQP"
# Setup OpenMDAO problem
prob.setup()
# Output N2 representation of OpenMDAO model
om.n2(prob, show_browser=False, outfile="beam_opt_n2.html")
# Run optimization
prob.run_driver()
After the optimization completes the user should see a print out to screen like shown below.
>>> Optimization terminated successfully (Exit mode 0)
>>> Current function value: 1.5534716448382722
>>> Iterations: 138
>>> Function evaluations: 372
>>> Gradient evaluations: 138
>>> Optimization Complete
>>> -----------------------------------
Once the optimization is complete we can post-process results.
We can write our optimized beam model to a BDF file so they can
be processed in other commonly used FEM software.
The f5
solution file at each optimization iteration can also be converted to a Tecplot or Paraview files using f5totec
or f5tovtk
, respectively.
# Write optimized structure to BDF
bdf_out = os.path.join(os.path.dirname(__file__), "beam_sol.bdf")
prob.model.tip_shear.coupling.write_bdf(bdf_out)
Finally, we can plot the optimized thickness distribution using matplotlib and compare against the expected optimal result from beam theory.
# Get optimized solution variables
x = prob.get_val("mesh.x_struct0", get_remote=True)[:-3:3]
t_opt = prob["dv_struct"]
m_opt = prob["tip_shear.mass"]
# Get analytical solution
t_exact = np.sqrt(6 * (L - x) * V / w / ys)
# Compute max thickness value
t0 = np.sqrt(6 * L * V / w / ys)
# Plot results for solution
plt.plot(x / L, t_opt / t0, "o", x, t_exact / t0)
plt.legend(["optimized", "analytical"])
plt.ylabel(r"$\frac{t(x)}{t_0}$", fontsize=16)
plt.xlabel(r"$\frac{x}{L}$", fontsize=16, labelpad=-5)
plt.title("Optimal beam thickness profile")
plt.text(0.05, 0.25, r"$t_0 = \sqrt{\frac{6VL}{w\sigma_y}}$", fontsize=12)
plt.show()