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 ofmetadataoptimization parameters.LevelOpt scenario is created using
LevelOptScenarioDescriptor. It takes the following shared inputs from previousScenarioDescriptorclasses:boundary_conditionsnote, with LevelOpt, multiple load cases can be input to a single optimization scenario using uniqueload_case_id. This allows LevelOpt to consider each load individually rather than the net load.# multiple load case optimization needs unique load_case_ids fixed_boundary1 = FixedBoundaryDescriptor() fixed_boundary1.boundary = MeshModel("fixed.ply") fixed_boundary1.load_case_id = 0 fixed_boundary2 = FixedBoundaryDescriptor() fixed_boundary2.boundary = MeshModel("fixed.ply") fixed_boundary2.load_case_id = 1 fixed_boundaries = [fixed_boundary1, fixed_boundary2] load1 = VectorForceDescriptor() load1.boundary = MeshModel("load1.ply") load1.direction = [1, 0, 0] load1.magnitude = 1000 load1.load_case_id = 0 load2 = VectorForceDescriptor() load2.boundary = MeshModel("load1.ply") load2.direction = [0, 1, 0] load2.magnitude = 1000 load2.load_case_id = 1 load_cases = [load1, load2]
internal_conditionsresolutionorcell_sizeunitssolver_typeused 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
metadataparameters specific to theLevelOptScenarioDescriptorinclude:vol_frac_cons(volume fraction constraint) sets the target volume of the final design as a fraction of the initial volume.voxelSize, or level set cell size, determines the size of the level set grid cells as a fraction of the FEA grid cell size.move_limitcontrols the extent of changes per optimization step, as a factor of thevoxelSize.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.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 multipleload_case_id. This enables individual load cases to be considered separately during optimization instead of a net load.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.Intact. Another option isOptimizerType.NLOpt.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.vol_frac_cons = 0.2 # 20% target volume 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 # override default optimizer
LevelOpt Optimization and Supported Queries¶
To run an optimization scenario a design domain
Material Domainmust first be defined. For assemblies, additional non-designMaterial Domain(s)need to be defined. Note, the design domainMeshModelrequires a uniqueinstance_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
LevelOptoptimization 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()
LevelOptalso has anoptimize()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
LevelOptIterationInfoobjects, one per load case, for the current design iteration. EachLevelOptIterationInfohas 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
GlobalQueryTypeclasses,ComplianceandVolumeFractionand twoFieldQueryclasses,BoundarySensitivityandBoundaryVelocity. 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)