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 shares the following inputs with other ScenarioDescriptor classes:

      • boundary_conditions. LevelOpt supports multiple load cases, which can be input to a single optimization scenario using unique load_case_id. This allows LevelOpt to consider each load individually rather than the net load.

        # multiple load case optimization needs unique load_case_ids
        auto fixed_boundary1 = std::make_shared<Intact::FixedBoundaryDescriptor>();
        fixed_boundary1->boundary = Intact::MeshModel("fixed.ply");
        fixed_boundary1->load_case_id = 0;
        
        auto fixed_boundary2 = std::make_shared<Intact::FixedBoundaryDescriptor>();
        fixed_boundary2->boundary = Intact::MeshModel("fixed.ply");
        fixed_boundary2->load_case_id = 1;
        
        auto load1 = std::make_shared<Intact::VectorForceDescriptor>();
        load1->boundary = Intact::MeshModel("load1.ply");
        load1->direction = {1, 0, 0};
        load1->magnitude = 1000;
        load1->load_case_id = 0;
        
        auto load2 = std::make_shared<Intact::VectorForceDescriptor>();
        load2->boundary = Intact::MeshModel("load1.ply");
        load2->direction = {1, 0, 0};
        load2->magnitude = 1000;
        load2->load_case_id = 1;
        
        auto load_cases = {fixed_boundary1, load1, fixed_boundary2, load2};
        
      • internal_conditions

      • resolution or cell_size

      • units

      • solver_type used in the initial linear elastic simulation with the following options:

        • MKL_PardisoLDLT (default) direct solver

        • AMGCL_amg_rigid_body iterative solver

      • basis_order used in the initial linear elastic simulation

LevelOpt Metadata Settings

  • The set of metadata parameters specific to the LevelOptScenarioDescriptor include:

    • 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_limit controls the extent of changes per optimization step, as a factor of the voxelSize.

    • 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_thickness specifies the region around boundary conditions that remains unchanged as a factor of level set grid cell size.

    • smooth_iter defines the frequency the geometry is smoothed during the optimization process as a number of iterations.

    • enable_fixed_interfaces allows specifying if the interface between the design domain (optimized) and non-design domain should be fully preserved. True preserves the interface and False allows material to be removed at the interface.

    • num_load_cases is a input required for an optimization scenario with multiple load_case_id. This enables individual load cases to be considered separately during optimization instead of a net load.

    • objective_functions sets 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 are ObjectiveFunctionType::Compliance (default) and ObjectiveFunctionType::MaximumStress.

      Note The MaximumStress objective 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 (finer cell_size or voxelSize) and a lower move_limit (less than 1.0, or approximately, 0.1 to 0.5). Further, we recommend increasing fix_thickness or creating non-design domain regions near boundaries.

    • optimizer_override allows overriding the default optimization solver. The default is OptimizerType::Intact. Another option is OptimizerType::NLOpt.

    • weights allows 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 weights metadata is currently in a beta stage. A 0.5/0.5 weighting between load cases, especially those with different objective_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.

    • output_directory is an optional input that specifies an output directory to write per-iteration optimization results. The results that are written per-iteration are the design written as a PLY file, a CSV file named iteration_history.txt that contains the iteration number, the load case number, the compliance and the volume fraction of the design.

       // 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;
       leveloptscenario.metadata.solver_override = Intact::SolverType::MKL_PardisoLDLT;   // direct LinearElastic solver
       leveloptscenario.metadata.basis_order = 1;                       // linear order for LinearElastic solver
       leveloptscenario.boundary_conditions = 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, Intact::ObjectiveFunctionType::MaximumStress}, {1, Intact::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 = Intact::OptimizerType::NLOpt; // override default optimizer
    

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 Domain

    • assembly or list of all design and non-design Material Domain(s)

    • LevelOptScenarioDescriptor which describes the optimization scenario parameters and inputs.

    • starting design MeshModel, or if no starting design is used, a nullptr argument.

       // 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);
    
       double sensitivity_value = r_s.get(0,0) // tuple of dimension 1 for each point
       double 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.

   // 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();