Mixed-Integer Truss Optimization with Juniper.jl

Description

This tutorial demonstrates mixed-integer truss topology optimization where every element’s density is restricted to exactly 0 or 1 (binary design). The mixed-integer nonlinear program (MINLP) is solved by Juniper.jl with IPOPT as the continuous relaxation solver, accessed through Nonconvex.jl.

This approach produces crisp black/white designs without intermediate densities, ideal for manufacturing where elements must be either present or absent.

Setup

using TopOpt, LinearAlgebra, StatsFuns
using NonconvexJuniper, NonconvexIpopt
using CairoMakie

Load Truss Geometry

Download the truss geometry file: tim_2d.json.

ndim = 2
node_points, elements, mats, crosssecs, fixities, load_cases = load_truss_json(
    joinpath(@__DIR__, "tim_$(ndim)d.json")
)
ndim, nnodes, ncells = length(node_points[1]), length(node_points), length(elements)
loads = load_cases["0"]
problem = TrussProblem(
    Val{:Linear}, node_points, elements, loads, fixities, mats, crosssecs
)

The JSON file contains node coordinates, element connectivity, supports, and load cases for a 2D truss ground structure.

Parameter Settings

xmin = 0.0001 # minimum density
x0 = fill(1.0, ncells) # initial design (all solid)
p = 1.0 # penalty exponent
V = 0.5 # maximum volume fraction (50%)

FEA Solver and Objective

solver = FEASolver(DirectSolver, problem; xmin=xmin)
comp = ComplianceFun(solver)

function obj(x)
    return comp(PseudoDensities(x))
end

function constr(x)
    return sum(x) / length(x) - V
end

Mixed-Integer Optimization

# Binary variables: integer=trues forces xᵢ ∈ {0, 1}
m = Model(obj)
addvar!(m, zeros(length(x0)), ones(length(x0)); integer=trues(length(x0)))
Nonconvex.add_ineq_constraint!(m, constr)

# Silence the Juniper branch-and-bound trace and IPOPT's per-node logs so
# the rendered tutorial stays readable.
options = JuniperIpoptOptions(
    log_levels = Symbol[],
    subsolver_options = IpoptOptions(; print_level = 0),
)
setpenalty!(solver, p)
@time r = Nonconvex.optimize(m, JuniperIpoptAlg(), x0; options=options)
******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
 Ipopt is released as open source code under the Eclipse Public License (EPL).
         For more information visit https://github.com/coin-or/Ipopt
******************************************************************************

Warning: The relaxation is only almost solved.
@ Juniper ~/.julia/packages/Juniper/93Umc/src/model.jl:113
Warning: Only almost solved
@ Juniper ~/.julia/packages/Juniper/93Umc/src/BnBTree.jl:116
Warning: Only almost solved
@ Juniper ~/.julia/packages/Juniper/93Umc/src/BnBTree.jl:116
 36.242461 seconds (77.86 M allocations: 4.257 GiB, 5.05% gc time, 87.54% compilation time: 5% of which was recompilation)
NonconvexJuniper.JuniperIpoptResult{Vector{Float64}, Float64, NonconvexCore.JuMPProblem{NonconvexCore.JuMPEvaluator{Vector{Float64}, Vector{Float64}, Vector{Float64}, Vector{Float64}, Vector{Float64}, NonconvexCore.CountingFunction{NonconvexCore.Objective{typeof(obj), Base.RefValue{Float64}}}, NonconvexCore.var"#eval_g#get_jump_problem##3"{NonconvexCore.VectorOfFunctions{Tuple{NonconvexCore.IneqConstraint{FunctionWrapper{typeof(constr)}, Float64}}}, Nothing}, NonconvexCore.var"#eval_grad_f#get_jump_problem##4"{NonconvexCore.CountingFunction{NonconvexCore.Objective{typeof(obj), Base.RefValue{Float64}}}}, NonconvexCore.var"#eval_jac_g#get_jump_problem##6"{NonconvexCore.VectorOfFunctions{Tuple{NonconvexCore.IneqConstraint{FunctionWrapper{typeof(constr)}, Float64}}}, Nothing}, NonconvexCore.var"#jac_structure#get_jump_problem##5"{Int64}, NonconvexCore.var"#get_jump_problem##7#get_jump_problem##8", NonconvexCore.var"#get_jump_problem##9#get_jump_problem##10"}, JuMP.Model, Vector{MathOptInterface.VariableIndex}}, Tuple{MathOptInterface.TerminationStatusCode, MathOptInterface.ResultStatusCode}, Int64}([1.0000000026525044, 0.0, 1.0, 1.0, 1.0000000026534592, 3.0574066958906664e-8, 3.5462333436054355e-8, 3.05740669633558e-8, 1.0000000026536169, 1.2728841767229526e-8  …  1.0000000054569695, 1.0000000054569695, 1.0000000026525044, 1.0000000026534592, 1.0000000026536169, 1.000000002653596, 1.0000000026535982, 1.0000000026536018, 1.0000000026535723, 1.000000002653795], 4.957003013317412, NonconvexCore.JuMPProblem{NonconvexCore.JuMPEvaluator{Vector{Float64}, Vector{Float64}, Vector{Float64}, Vector{Float64}, Vector{Float64}, NonconvexCore.CountingFunction{NonconvexCore.Objective{typeof(obj), Base.RefValue{Float64}}}, NonconvexCore.var"#eval_g#get_jump_problem##3"{NonconvexCore.VectorOfFunctions{Tuple{NonconvexCore.IneqConstraint{FunctionWrapper{typeof(constr)}, Float64}}}, Nothing}, NonconvexCore.var"#eval_grad_f#get_jump_problem##4"{NonconvexCore.CountingFunction{NonconvexCore.Objective{typeof(obj), Base.RefValue{Float64}}}}, NonconvexCore.var"#eval_jac_g#get_jump_problem##6"{NonconvexCore.VectorOfFunctions{Tuple{NonconvexCore.IneqConstraint{FunctionWrapper{typeof(constr)}, Float64}}}, Nothing}, NonconvexCore.var"#jac_structure#get_jump_problem##5"{Int64}, NonconvexCore.var"#get_jump_problem##7#get_jump_problem##8", NonconvexCore.var"#get_jump_problem##9#get_jump_problem##10"}, JuMP.Model, Vector{MathOptInterface.VariableIndex}}(NonconvexCore.JuMPEvaluator{Vector{Float64}, Vector{Float64}, Vector{Float64}, Vector{Float64}, Vector{Float64}, NonconvexCore.CountingFunction{NonconvexCore.Objective{typeof(obj), Base.RefValue{Float64}}}, NonconvexCore.var"#eval_g#get_jump_problem##3"{NonconvexCore.VectorOfFunctions{Tuple{NonconvexCore.IneqConstraint{FunctionWrapper{typeof(constr)}, Float64}}}, Nothing}, NonconvexCore.var"#eval_grad_f#get_jump_problem##4"{NonconvexCore.CountingFunction{NonconvexCore.Objective{typeof(obj), Base.RefValue{Float64}}}}, NonconvexCore.var"#eval_jac_g#get_jump_problem##6"{NonconvexCore.VectorOfFunctions{Tuple{NonconvexCore.IneqConstraint{FunctionWrapper{typeof(constr)}, Float64}}}, Nothing}, NonconvexCore.var"#jac_structure#get_jump_problem##5"{Int64}, NonconvexCore.var"#get_jump_problem##7#get_jump_problem##8", NonconvexCore.var"#get_jump_problem##9#get_jump_problem##10"}(42, [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  …  0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0  …  1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0  …  1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], 1, [-Inf], [0.0], 42, 0, NonconvexCore.CountingFunction{NonconvexCore.Objective{typeof(obj), Base.RefValue{Float64}}}(Base.RefValue{Int64}(17906), NonconvexCore.Objective{typeof(obj), Base.RefValue{Float64}}(obj, Base.RefValue{Float64}(1.0), Set{Symbol}())), NonconvexCore.var"#eval_g#get_jump_problem##3"{NonconvexCore.VectorOfFunctions{Tuple{NonconvexCore.IneqConstraint{FunctionWrapper{typeof(constr)}, Float64}}}, Nothing}(NonconvexCore.VectorOfFunctions{Tuple{NonconvexCore.IneqConstraint{FunctionWrapper{typeof(constr)}, Float64}}}((NonconvexCore.IneqConstraint{FunctionWrapper{typeof(constr)}, Float64}(FunctionWrapper{typeof(constr)}(constr, 1), 0.0, 1, Set{Symbol}()),)), nothing, Core.Box(1)), NonconvexCore.var"#eval_grad_f#get_jump_problem##4"{NonconvexCore.CountingFunction{NonconvexCore.Objective{typeof(obj), Base.RefValue{Float64}}}}(NonconvexCore.CountingFunction{NonconvexCore.Objective{typeof(obj), Base.RefValue{Float64}}}(Base.RefValue{Int64}(17906), NonconvexCore.Objective{typeof(obj), Base.RefValue{Float64}}(obj, Base.RefValue{Float64}(1.0), Set{Symbol}()))), NonconvexCore.var"#eval_jac_g#get_jump_problem##6"{NonconvexCore.VectorOfFunctions{Tuple{NonconvexCore.IneqConstraint{FunctionWrapper{typeof(constr)}, Float64}}}, Nothing}(NonconvexCore.VectorOfFunctions{Tuple{NonconvexCore.IneqConstraint{FunctionWrapper{typeof(constr)}, Float64}}}((NonconvexCore.IneqConstraint{FunctionWrapper{typeof(constr)}, Float64}(FunctionWrapper{typeof(constr)}(constr, 1), 0.0, 1, Set{Symbol}()),)), nothing, Core.Box(42)), NonconvexCore.var"#jac_structure#get_jump_problem##5"{Int64}(42, Core.Box(nothing), Core.Box(42), Core.Box(1), Core.Box([0.023809523809523808 0.023809523809523808 … 0.023809523809523808 0.023809523809523808])), NonconvexCore.var"#get_jump_problem##7#get_jump_problem##8"(), NonconvexCore.var"#get_jump_problem##9#get_jump_problem##10"()), A JuMP Model
├ solver: Juniper
├ objective_sense: MIN_SENSE
│ └ objective_function_type: JuMP.AffExpr
├ num_variables: 42
├ num_constraints: 126
│ ├ JuMP.VariableRef in MOI.GreaterThan{Float64}: 42
│ ├ JuMP.VariableRef in MOI.LessThan{Float64}: 42
│ └ JuMP.VariableRef in MOI.ZeroOne: 42
└ Names registered in the model: none, MathOptInterface.VariableIndex[MOI.VariableIndex(1), MOI.VariableIndex(2), MOI.VariableIndex(3), MOI.VariableIndex(4), MOI.VariableIndex(5), MOI.VariableIndex(6), MOI.VariableIndex(7), MOI.VariableIndex(8), MOI.VariableIndex(9), MOI.VariableIndex(10)  …  MOI.VariableIndex(33), MOI.VariableIndex(34), MOI.VariableIndex(35), MOI.VariableIndex(36), MOI.VariableIndex(37), MOI.VariableIndex(38), MOI.VariableIndex(39), MOI.VariableIndex(40), MOI.VariableIndex(41), MOI.VariableIndex(42)]), (MathOptInterface.LOCALLY_SOLVED, MathOptInterface.FEASIBLE_POINT), 17906)

Juniper.jl uses branch-and-bound with IPOPT solving continuous relaxations at each node. The integer flag enforces binary design variables.

Results

@show obj(r.minimizer)
@show constr(r.minimizer)

# Verify binary design
x = r.minimizer
println("Binary design: $(all(xᵢ -> xᵢ < 0.01 || xᵢ > 0.99, x))")
println("Elements removed: $(sum(x .< 0.5)) / $ncells")
obj(r.minimizer) = 4.957003013317412
constr(r.minimizer) = 9.642689313693609e-9
Binary design: true
Elements removed: 21 / 42

Visualization

fig = visualize(problem; solver.u, topology=r.minimizer, default_exagg_scale=0.0)
Figure 1: Binary truss design: elements either present (black) or removed (white)

The result shows a true binary truss layout — optimal load paths with no intermediate densities, ready for manufacturing.