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.// multiple load case optimization using load_case_ids auto fixed_boundary = std::make_shared<Intact::FixedBoundaryDescriptor>(); fixed_boundary->boundary = Intact::MeshModel("fixed.ply"); // applies restraint to both load cases fixed_boundary->load_case_ids = {0, 1}; auto load1 = std::make_shared<Intact::VectorForceDescriptor>(); load1->boundary = Intact::MeshModel("load1.ply"); load1->direction = {1, 0, 0}; load1->magnitude = 1000; // applies to load case 0 only load1->load_case_ids = {0}; auto load2 = std::make_shared<Intact::VectorForceDescriptor>(); load2->boundary = Intact::MeshModel("load2.ply"); load2->direction = {0, 1, 0}; load2->magnitude = 1000; // applies to load case 1 only load2->load_case_ids = {1}; auto load_cases = {fixed_boundary, 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
Intact::LevelOptScenarioDescriptor leveloptscenario;
// Standard scenario parameters
leveloptscenario.materials = {{"Aluminum": material}};
leveloptscenario.metadata.cell_size = 2.5;
leveloptscenario.metadata.units = Intact::UnitSystem::MeterKilogramSecond;
// Use direct linear solver
leveloptscenario.metadata.solver_override = Intact::SolverType::MKL_PardisoLDLT;
// Use linear elements
leveloptscenario.metadata.basis_order = 1;
// The boundary conditions have two load cases
leveloptscenario.boundary_conditions = load_cases;
// LevelOpt specific metadata
leveloptscenario.optimization_metadata.voxelSize = 0.5;
leveloptscenario.optimization_metadata.move_limit = 1.0;
// Perform 10 optimization iterations
leveloptscenario.optimization_metadata.opt_max_iter = 10;
leveloptscenario.optimization_metadata.fix_thickness = 4;
// Smooth the design each iteration
leveloptscenario.optimization_metadata.smooth_iter = 1;
leveloptscenario.optimization_metadata.enable_fixed_interfaces = true;
// There are two load cases
leveloptscenario.optimization_metadata.num_load_cases = 2;
// First load case uses MaximumStress objective, second load case uses Compliance objective
leveloptscenario.optimization_metadata.objective_functions = {{0, Intact::ObjectiveFunctionType::MaximumStress}, {1, Intact::ObjectiveFunctionType::Compliance}};
// Set weights for each load case
leveloptscenario.optimization_metadata.weights = {{0, 10}, {1, 1}};
leveloptscenario.optimization_metadata.optimizer_override = Intact::OptimizerType::NLOpt;
// 20% target volume
Intact::OptimizationConstraint constraint1(Intact::OptimizationConstraintType::MaximumVolumeFraction, 0.20);
leveloptscenario.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
auto design_geometry = Intact::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
auto design_domain = Intact::MaterialDomain(design_geometry, "Aluminum", leveloptscenario);
component1 = Intact::MaterialDomain(body1, "Aluminum", leveloptscenario);
component2 = Intact::MaterialDomain(body2, "Aluminum", leveloptscenario);
Intact::Assembly 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
auto optimizer = Intact::LevelOpt(design_domain, assembly, leveloptscenario, nullptr);
optimizer.optimize();
// Or, create and run the simulation with optional starting design
auto starting_design = Intact::MeshModel("starting_design.ply");
optimizer = Intact::LevelOpt(design_domain, assembly, leveloptscenario, starting_design);
optimizer.optimize();
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
Intact::GlobalQuery compliance_query = Intact::GlobalQuery(Intact::GlobalQueryType::Compliance, Intact::DiscreteIndex(iteration));
Intact::GlobalQuery volume_fraction_query = Intact::GlobalQuery(Intact::GlobalQueryType::VolumeFraction, Intact::DiscreteIndex(iteration));
auto compliance = optimizer.sample(compliance_query);
auto volume_fraction = optimizer.sample(volume_fraction_query);
double compliance_value = compliance.get(0, 0);
double volume_fraction_value = volume_fraction.get(0, 0);
// LevelOpt field queries
auto boundary_sensitivity_query = Intact::FieldQuery(Intact::Field::BoundarySensitivity, Intact::DiscreteIndex(0));
auto boundary_velocity_query = Intact::FieldQuery(Intact::Field::BoundaryVelocity, Intact::DiscreteIndex(0));
// no QueryResult() object, because boundary sensitivity is only defined on design boundary
auto r_s = optimizer.sample(boundary_sensitivity_query);
// no QueryResult() object, because boundary velocity is only defined on design boundary
auto r_v = optimizer.sample(boundary_velocity_query);
// tuple of dimension 1 for each point
double sensitivity_value = r_s.get(0, 0)
// tuple of dimension 1 for each point
double velocity_value = r_v.get(0, 0)
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
auto optimizer = LevelOpt(design_domain, assembly, leveloptscenario, nullptr);
optimizer.optimize();
// get the design iterations from the optimization scenario
auto designs = optimizer.getDesigns();
// write the design output mesh .ply files to desired directory
for (int i = 0; i < designs.size() i++) {
designs[i].writePLY("optimized_design_" + std::to_string(i) + ".ply");
}