How To: LevelOpt¶
LevelOpt is another ScenarioDescriptor (LevelOptScenarioDescriptor)
for level set structural topology optimization. It supports all of the standard
linear elastic boundary conditions and a variety of metadata optimization parameters.
LevelOpt scenario is created using
LevelOptScenarioDescriptor. It takes the following shared inputs from previousScenarioDescriptorclasses:boundary_conditionsinternal_conditionsNote
LevelOpt supports multiple load cases as input to a single optimization scenario. Multiple load cases allows LevelOpt to consider each load individually rather than the net load. Multiple load cases are created by specifying optional
load_case_idsfield on each boundary condition descriptor and internal condition descriptor. Theload_case_idsis a list of the load cases that this boundary condition belongs to.# fixed is a boundary condition for load cases 0 and 1 fixed = FixedBoundaryDescriptor() fixed.boundary = MeshModel("fixed.ply") fixed.load_case_ids = [0, 1] # load1 is a boundary condition for load case 0 only load1 = VectorForceDescriptor() load1.boundary = MeshModel("load1.ply") load1.direction = [1, 0, 0] load1.magnitude = 1000 load1.load_case_ids = [0] # load2 is a boundary condition for load case 1 only load2 = VectorForceDescriptor() load2.boundary = MeshModel("load2.ply") load2.direction = [0, 1, 0] load2.magnitude = 1000 load2.load_case_ids = [1] scenario.boundary_conditions = [fixed, load1, load2]
Deprecated since version 1.1.18: The
load_case_idproperty is deprecated. Useload_case_idsinstead.load_case_idaccepts a single integer, whileload_case_idsaccepts a list. The two properties cannot be mixed in the same script.resolutionorcell_sizeunitssolver_overrideused in the initial linear elastic simulation with the following options:MKL_PardisoLDLT(default) direct solverAMGCL_amg_rigid_bodyiterative solver
basis_orderused in the initial linear elastic simulation
LevelOpt Metadata Settings¶
The set of metadata parameters specific to the LevelOptScenarioDescriptor include:
constraintsa collection of constraints on the optimization procedure. A maximum volume fraction constraint is required. Multiple constraints can be applied. Constraints can limit the maximum von Mises stress or the minimum frequency of the first vibration mode.Deprecated since version 2.0.2: The
vol_frac_cons(volume fraction constraint) field is deprecated. Add anOptimizationConstraintwith aconstraint_typeofOptimizationConstraintType.MaximumVolumeFractionto theconstraintsfield instead.voxelSize, or level set cell size, determines the size of the level set grid cells as a fraction of the FEA grid cell size.Tip
Recommended values between 0.25 and 1.0. Smaller values create finer, more detailed features but take longer to optimize.
move_limitcontrols the extent of changes per optimization step, as a factor of thevoxelSize.Tip
Recommended values between 0.5 and 2.0. Smaller values move the design’s boundary more slowly, evolving the design more gradually.
opt_max_iter(optimization max iterations) sets the maximum number of iterations for the optimization process. Each iteration refines the design by updating the topology based on the objective function and constraints.fix_thicknessspecifies the region around boundary conditions that remains unchanged as a factor of level set grid cell size.Tip
Recommended values between 2.0 and 10.0. Higher values preserve larger regions around applied loads and restraints, ensuring those regions are not altered during optimization.
smooth_iterdefines the frequency the geometry is smoothed during the optimization process as a number of iterations.enable_fixed_interfacesallows specifying if the interface between the design domain (optimized) and non-design domain should be fully preserved.Truepreserves the interface andFalseallows material to be removed at the interface.num_load_casesis a input required for an optimization scenario with multiple load cases. This enables individual load cases to be considered separately during optimization instead of a net load. The number should match the total number of unique load case IDs specified inload_case_ids.objective_functionssets the objective function for the optimization. This is a dictionary where the key is the load case ID and the value is the objective function type. Supported types areObjectiveFunctionType.Compliance(default) andObjectiveFunctionType.MaximumStress.Note
The
MaximumStressobjective function is currently in a beta stage. Due to the nature of stress optimization, it may not be suitable for all problems. It performs best on problems with clear stress concentrations away from restraints and boundary conditions. Using it in other cases may lead to non-intuitive or unusable designs. For best results with stress optimization, we recommend using higher fidelity models (finercell_sizeorvoxelSize) and a lowermove_limit(less than 1.0, or approximately, 0.1 to 0.5). Further, we recommend increasingfix_thicknessor creating non-design domain regions near boundaries.optimizer_overrideallows overriding the default optimization solver. The default isOptimizerType.NLOpt, which will use an optimizer from the NLOpt collection of optimizers. Another option isOptimizerType.Intact.weightsallows specifying the weight for each load case in a multi-load case optimization. This is a dictionary where the key is the load case ID and the value is the weight.Note
The
weightsmetadata is currently in a beta stage. A 0.5/0.5 weighting between load cases, especially those with differentobjective_functions, may not result in an equal balance. Any weight can be input, and it is useful to query the boundary sensitivities to understand the order of magnitude for each load case’s sensitivity to inform the weighting. Using compliance to stabilize the optimization can also lead to better designs; specifically, using a small compliance weight (e.g., 1e-3 to 1e-5) relative to a 1.0 weight for a stress objective can lead to more stable designs while still minimizing stress.
# Setup the LevelOpt optimization scenario descriptor
leveloptscenario = LevelOptScenarioDescriptor()
# Standard scenario parameters
leveloptscenario.materials = {"Aluminum": material}
leveloptscenario.metadata.cell_size = 2.5
leveloptscenario.metadata.units = UnitSystem.MeterKilogramSecond
leveloptscenario.metadata.solver_override = "MKL_PardisoLDLT" # direct LinearElastic solver
leveloptscenario.metadata.basis_order = 1 # linear order for LinearElastic solver
leveloptscenario.boundary_conditions = fixed_boundaries + load_cases # consist of two sets of load cases
# LevelOpt specific metadata
leveloptscenario.optimization_metadata.voxelSize = 0.5 # 0.5 (default) level set cell size factor
leveloptscenario.optimization_metadata.move_limit = 1.0 # 1.0 (default) move limit factor
leveloptscenario.optimization_metadata.opt_max_iter = 10 # 10 design iterations
leveloptscenario.optimization_metadata.fix_thickness = 4 # 4 (default) level set cells which are unchanged near BCs
leveloptscenario.optimization_metadata.smooth_iter = 1 # smoothing performed at every 1 iteration
leveloptscenario.optimization_metadata.enable_fixed_interfaces = True # retain full contact between assembly components
leveloptscenario.optimization_metadata.num_load_cases = 2 # 2 load cases created
leveloptscenario.optimization_metadata.objective_functions = {0: ObjectiveFunctionType.MaximumStress, 1: ObjectiveFunctionType.Compliance} # set objectives for each load case
leveloptscenario.optimization_metadata.weights = {0: 10, 1: 1} # set weights for each load case
leveloptscenario.optimization_metadata.optimizer_override = OptimizerType.NLOpt
# 20% target volume
constraint1 = OptimizationConstraint(
constraint_type=OptimizationConstraintType.MaximumVolumeFraction, target=0.2
)
scenario.optimization_metadata.constraints = [constraint1]
LevelOpt Optimization and Supported Queries¶
To run an optimization scenario a design domain Material Domain must first be
defined. For assemblies, additional non-design Material Domain(s) need to be defined.
Note, the design domain MeshModel requires a unique instance_id.
# make sure to include instance_id for design_geometry/design_domain
design_geometry = MeshModel("design_domain.stl")
design_geometry.instance_id = "ex_design_domain" # can be any unique id/name
design_geometry.refine()
# Create material domains and assembly
design_domain = MaterialDomain(design_geometry, "Aluminum", leveloptscenario)
component1 = MaterialDomain(body1, "Aluminum", leveloptscenario)
component2 = MaterialDomain(body2, "Aluminum", leveloptscenario)
assembly = [design_domain, component1, component2]
The LevelOpt optimization solver then takes the following arguments:
design
Material Domainassembly or list of all design and non-design
Material Domain(s)LevelOptScenarioDescriptorwhich describes the optimization scenario parameters and inputs.starting design
MeshModel, or if no starting design is used, aNoneargument.
# Create and run the simulation
optimizer = LevelOpt(design_domain, assembly, leveloptscenario, None)
optimizer.optimize()
# Create and run the simulation with optional starting design
starting_design = MeshModel("starting_design.ply")
optimizer = LevelOpt(design_domain, assembly, leveloptscenario, starting_design)
optimizer.optimize()
LevelOpt also has an optimize() override that takes a callback function that operates on per-iteration data.
The following example defines a callback function that writes the current iteration’s design to a file:
# An example showing how to use a callback function to LevelOpt.optimize().
def callback(stat: list[LevelOptIterationInfo], model: MeshModel):
iteration_number = stat[0].iteration
model.writePLY(f"design_{iteration_number}.ply")
# code to setup scenario not shown
# Create and run the simulation
optimizer = LevelOpt(design_domain, assembly, leveloptscenario, None)
optimizer.optimize(callback)
The first argument is a list of LevelOptIterationInfo objects, one per load case,
for the current design iteration. Each LevelOptIterationInfo has the compliance,
iteration number, load case identifier, maximum displacement, maximum von Mises stress,
and the volume fraction for the current design.
LevelOpt Queries include support for two GlobalQueryType classes,
Compliance and VolumeFraction and two FieldQuery classes,
BoundarySensitivity and BoundaryVelocity.
A discrete index argument corresponding to the optimization iteration is required to query for any of the previously described quantities.
# LevelOpt global queries
compliance_query = GlobalQuery(GlobalQueryType.Compliance, DiscreteIndex(iteration))
volume_fraction_query = GlobalQuery(GlobalQueryType.VolumeFraction, DiscreteIndex(iteration))
compliance = optimizer.sample(compliance_query)
volume_fraction = optimizer.sample(volume_fraction_query)
compliance_value = compliance.get(0,0)
volume_fraction_value = volume_fraction.get(0,0)
# LevelOpt field queries
boundary_sensitivity_query = FieldQuery(f=Field.BoundarySensitivity, scheme=DiscreteIndex(0))
boundary_velocity_query = FieldQuery(f=Field.BoundaryVelocity, scheme=DiscreteIndex(0))
r_s = optimizer.sample(boundary_sensitivity_query) # no QueryResult() object, only defined on design boundary
r_v = optimizer.sample(boundary_velocity_query) # no QueryResult() object, only defined on design boundary
sensitivity_value = r_s.get(0,0) # tuple of dimension 1 for each point
velocity_value = r_v.get(0,0) # tuple of dimension 1 for each point
Output mesh designs can be stored using .getDesigns() from the LevelOpt scenario optimization
and saved optionally as .ply files in a desired directory with .writePLY()
# run LevelOpt Scenario
optimizer = LevelOpt(design_domain, assembly, leveloptscenario, None)
optimizer.optimize()
# get the design iterations from the optimization scenario
designs = optimizer.getDesigns()
# write the design output mesh .ply files to desired directory
for i, design in enumerate(designs):
ply_filename = os.path.join(ply_dir, f"optimized_design_{i}.ply")
design.writePLY(ply_filename)