using TopOpt, LinearAlgebra, StatsFuns
using NonconvexJuniper, NonconvexIpopt
using CairoMakieMixed-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
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
endMixed-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)The result shows a true binary truss layout — optimal load paths with no intermediate densities, ready for manufacturing.