using NonconvexTOBS, TopOpt
using WGLMakie
WGLMakie.activate!(; resize_to=:parent)
using Bonito
if haskey(ENV, "QUARTO_PROJECT_DIR")
Bonito.Page(exportable=true, offline=true)
else
Bonito.browser_display()
end
display_app(app) = display(app)TOBS: Topological Optimization of Binary Structures
Description
TOBS (Topological Optimization of Binary Structures) is a heuristic algorithm for binary topology optimization. Unlike SIMP which uses continuous densities, TOBS directly optimizes 0/1 (void/solid) designs by linearizing the objective and constraints, then solving a binary nonlinear program. The default solver is Cbc.jl (COIN-OR Branch and Cut), which determines which binary variables must be flipped in each iteration.
This tutorial demonstrates TOBS on a 2D cantilever beam with 80×50 elements (4,000 design variables). The algorithm efficiently handles the combinatorial challenge of binary optimization through sequential linearization and branch- and-cut solving.
Setup
WGLMakie.activate!(; resize_to=:parent) selects the browser renderer and fills the Quarto output column. Bonito.Page(exportable=true, offline=true) embeds the assets needed by visualize(...; static=true) in the Quarto output, so the visualization does not require a running Julia process.
Problem Definition
We use a large 2D cantilever beam to demonstrate TOBS scalability:
E = 1.0 # Young's modulus
v = 0.3 # Poisson's ratio
f = 1.0 # downward force
rmin = 6.0 # filter radius
xmin = 0.001 # minimum density
V = 0.5 # maximum volume fraction
p = 3.0 # topological optimization penalty
problem_size = (80, 50) # 4,000 elements
x0 = fill(1.0, prod(problem_size)) # start from fully solid design
problem = PointLoadCantilever(problem_size, (1.0, 1.0), E, v, f)The mesh (80×50) tests TOBS ability to handle high-dimensional binary optimization problems.
FEA Solver and Filter
solver = FEASolver(DirectSolver, problem; xmin=xmin)
cheqfilter = DensityFilterFun(solver; rmin=rmin) # mesh-independent filtering
comp = ComplianceFun(solver)The density filter ensures smooth designs and prevents checkerboarding, critical for binary optimization where small features can cause numerical instability.
Objective and Constraint
obj(x) = comp(cheqfilter(PseudoDensities(x))) # compliance objective
constr(x) = sum(cheqfilter(PseudoDensities(x))) / length(x) - V # volume constraintThe objective minimizes compliance (strain energy) while the constraint limits material usage to 50% of the domain volume.
Optimization Setup
m = Model(obj)
addvar!(m, zeros(length(x0)), ones(length(x0))) # bounds: 0 ≤ xᵢ ≤ 1
Nonconvex.add_ineq_constraint!(m, constr)
# TOBS starts from the provided x0 and flips at most
# `movelimit * numVars` variables per iteration, so reaching the 50%
# volume target from 100% solid needs enough iterations to remove half
# the material.
options = TOBSOptions(; maxiter=100, movelimit=0.05)
setpenalty!(solver, p)TOBS uses binary variables (0 or 1) with bound constraints. The penalty exponent p=3.0 penalizes intermediate densities in the FEA solution.
Run TOBS Optimization
@time r = Nonconvex.optimize(m, TOBSAlg(), x0; options=options)[ Info: iter = 0, obj = 21.529, constr_vio_norm = 0.5, er = 1.0 [ Info: iter = 1, obj = 22.23, constr_vio_norm = 0.45, er = 1.0 [ Info: iter = 2, obj = 23.359, constr_vio_norm = 0.405, er = 1.0 [ Info: iter = 3, obj = 24.142, constr_vio_norm = 0.364, er = 1.0 [ Info: iter = 4, obj = 25.645, constr_vio_norm = 0.328, er = 1.0 [ Info: iter = 5, obj = 26.606, constr_vio_norm = 0.295, er = 1.0 [ Info: iter = 6, obj = 27.69, constr_vio_norm = 0.265, er = 1.0 [ Info: iter = 7, obj = 28.892, constr_vio_norm = 0.239, er = 1.0 [ Info: iter = 8, obj = 31.086, constr_vio_norm = 0.215, er = 1.0 [ Info: iter = 9, obj = 31.249, constr_vio_norm = 0.193, er = 1.0 [ Info: iter = 10, obj = 34.579, constr_vio_norm = 0.174, er = 1.0 [ Info: iter = 11, obj = 34.558, constr_vio_norm = 0.157, er = 1.0 [ Info: iter = 12, obj = 35.402, constr_vio_norm = 0.141, er = 1.0 [ Info: iter = 13, obj = 36.849, constr_vio_norm = 0.127, er = 1.0 [ Info: iter = 14, obj = 37.997, constr_vio_norm = 0.114, er = 1.0 [ Info: iter = 15, obj = 37.535, constr_vio_norm = 0.102, er = 1.0 [ Info: iter = 16, obj = 38.72, constr_vio_norm = 0.092, er = 1.0 [ Info: iter = 17, obj = 40.353, constr_vio_norm = 0.083, er = 1.0 [ Info: iter = 18, obj = 40.063, constr_vio_norm = 0.075, er = 1.0 [ Info: iter = 19, obj = 42.345, constr_vio_norm = 0.067, er = 1.0 [ Info: iter = 20, obj = 42.221, constr_vio_norm = 0.061, er = 1.0 [ Info: iter = 21, obj = 42.459, constr_vio_norm = 0.054, er = 0.031 [ Info: iter = 22, obj = 46.785, constr_vio_norm = 0.049, er = 0.035 [ Info: iter = 23, obj = 44.547, constr_vio_norm = 0.044, er = 0.035 [ Info: iter = 24, obj = 45.476, constr_vio_norm = 0.04, er = 0.034 [ Info: iter = 25, obj = 46.758, constr_vio_norm = 0.036, er = 0.033 [ Info: iter = 26, obj = 47.959, constr_vio_norm = 0.032, er = 0.032 [ Info: iter = 27, obj = 47.516, constr_vio_norm = 0.029, er = 0.029 [ Info: iter = 28, obj = 47.875, constr_vio_norm = 0.026, er = 0.029 [ Info: iter = 29, obj = 55.551, constr_vio_norm = 0.023, er = 0.033 [ Info: iter = 30, obj = 52.09, constr_vio_norm = 0.021, er = 0.037 [ Info: iter = 31, obj = 47.932, constr_vio_norm = 0.019, er = 0.04 [ Info: iter = 32, obj = 54.087, constr_vio_norm = 0.016, er = 0.044 [ Info: iter = 33, obj = 49.284, constr_vio_norm = 0.015, er = 0.048 [ Info: iter = 34, obj = 49.725, constr_vio_norm = 0.013, er = 0.047 [ Info: iter = 35, obj = 51.316, constr_vio_norm = 0.012, er = 0.047 [ Info: iter = 36, obj = 49.305, constr_vio_norm = 0.011, er = 0.047 [ Info: iter = 37, obj = 49.376, constr_vio_norm = 0.01, er = 0.046 [ Info: iter = 38, obj = 50.668, constr_vio_norm = 0.009, er = 0.044 [ Info: iter = 39, obj = 50.57, constr_vio_norm = 0.008, er = 0.044 [ Info: iter = 40, obj = 51.528, constr_vio_norm = 0.007, er = 0.044 [ Info: iter = 41, obj = 51.873, constr_vio_norm = 0.006, er = 0.04 [ Info: iter = 42, obj = 49.703, constr_vio_norm = 0.006, er = 0.04 [ Info: iter = 43, obj = 51.258, constr_vio_norm = 0.005, er = 0.04 [ Info: iter = 44, obj = 50.679, constr_vio_norm = 0.004, er = 0.039 [ Info: iter = 45, obj = 50.077, constr_vio_norm = 0.004, er = 0.038 [ Info: iter = 46, obj = 52.072, constr_vio_norm = 0.004, er = 0.04 [ Info: iter = 47, obj = 49.823, constr_vio_norm = 0.003, er = 0.042 [ Info: iter = 48, obj = 51.636, constr_vio_norm = 0.003, er = 0.036 [ Info: iter = 49, obj = 50.133, constr_vio_norm = 0.002, er = 0.034 [ Info: iter = 50, obj = 49.342, constr_vio_norm = 0.002, er = 0.031 [ Info: iter = 51, obj = 53.679, constr_vio_norm = 0.002, er = 0.029 [ Info: iter = 52, obj = 49.833, constr_vio_norm = 0.002, er = 0.028 [ Info: iter = 53, obj = 50.839, constr_vio_norm = 0.001, er = 0.028 [ Info: iter = 54, obj = 50.607, constr_vio_norm = 0.001, er = 0.027 [ Info: iter = 55, obj = 50.948, constr_vio_norm = 0.001, er = 0.025 [ Info: iter = 56, obj = 55.663, constr_vio_norm = 0.001, er = 0.03 [ Info: iter = 57, obj = 57.099, constr_vio_norm = 0.001, er = 0.03 [ Info: iter = 58, obj = 54.438, constr_vio_norm = 0.001, er = 0.032 [ Info: iter = 59, obj = 68.036, constr_vio_norm = 0.001, er = 0.044 [ Info: iter = 60, obj = 54.831, constr_vio_norm = 0.001, er = 0.056 [ Info: iter = 61, obj = 61.941, constr_vio_norm = 0.0, er = 0.06 [ Info: iter = 62, obj = 58.666, constr_vio_norm = 0.0, er = 0.061 [ Info: iter = 63, obj = 56.825, constr_vio_norm = 0.0, er = 0.062 [ Info: iter = 64, obj = 55.107, constr_vio_norm = 0.0, er = 0.063 [ Info: iter = 65, obj = 61.766, constr_vio_norm = 0.0, er = 0.066 [ Info: iter = 66, obj = 53.427, constr_vio_norm = 0.0, er = 0.072 [ Info: iter = 67, obj = 57.127, constr_vio_norm = 0.0, er = 0.073 [ Info: iter = 68, obj = 55.687, constr_vio_norm = 0.0, er = 0.073 [ Info: iter = 69, obj = 57.492, constr_vio_norm = 0.0, er = 0.073 [ Info: iter = 70, obj = 56.906, constr_vio_norm = 0.0, er = 0.069 [ Info: iter = 71, obj = 57.688, constr_vio_norm = 0.0, er = 0.066 [ Info: iter = 72, obj = 52.09, constr_vio_norm = 0.0, er = 0.07 [ Info: iter = 73, obj = 56.572, constr_vio_norm = 0.0, er = 0.074 [ Info: iter = 74, obj = 54.006, constr_vio_norm = 0.0, er = 0.075 [ Info: iter = 75, obj = 54.037, constr_vio_norm = 0.0, er = 0.071 [ Info: iter = 76, obj = 54.663, constr_vio_norm = 0.0, er = 0.07 [ Info: iter = 77, obj = 53.534, constr_vio_norm = 0.0, er = 0.069 [ Info: iter = 78, obj = 54.906, constr_vio_norm = 0.0, er = 0.058 [ Info: iter = 79, obj = 52.861, constr_vio_norm = 0.0, er = 0.049 [ Info: iter = 80, obj = 52.528, constr_vio_norm = 0.0, er = 0.043 [ Info: iter = 81, obj = 52.977, constr_vio_norm = 0.0, er = 0.041 [ Info: iter = 82, obj = 52.805, constr_vio_norm = 0.0, er = 0.04 [ Info: iter = 83, obj = 51.58, constr_vio_norm = 0.0, er = 0.039 [ Info: iter = 84, obj = 52.574, constr_vio_norm = 0.0, er = 0.034 [ Info: iter = 85, obj = 51.36, constr_vio_norm = 0.0, er = 0.028 [ Info: iter = 86, obj = 49.835, constr_vio_norm = 0.0, er = 0.026 [ Info: iter = 87, obj = 50.553, constr_vio_norm = 0.0, er = 0.026 [ Info: iter = 88, obj = 50.681, constr_vio_norm = 0.0, er = 0.024 [ Info: iter = 89, obj = 49.318, constr_vio_norm = 0.0, er = 0.025 [ Info: iter = 90, obj = 49.594, constr_vio_norm = 0.0, er = 0.025 [ Info: iter = 91, obj = 48.694, constr_vio_norm = 0.0, er = 0.021 [ Info: iter = 92, obj = 51.35, constr_vio_norm = 0.0, er = 0.019 [ Info: iter = 93, obj = 48.783, constr_vio_norm = 0.0, er = 0.019 [ Info: iter = 94, obj = 49.639, constr_vio_norm = 0.0, er = 0.02 [ Info: iter = 95, obj = 49.263, constr_vio_norm = 0.0, er = 0.02 [ Info: iter = 96, obj = 48.696, constr_vio_norm = 0.0, er = 0.019 [ Info: iter = 97, obj = 48.803, constr_vio_norm = 0.0, er = 0.018 [ Info: iter = 98, obj = 51.011, constr_vio_norm = 0.0, er = 0.018 [ Info: iter = 99, obj = 48.809, constr_vio_norm = 0.0, er = 0.02 ┌ Warning: No feasible solution was found during the optimization. Returning the best design found. └ @ NonconvexTOBS ~/.julia/packages/NonconvexTOBS/bPRXy/src/NonconvexTOBS.jl:230 115.361569 seconds (75.49 M allocations: 5.863 GiB, 2.75% gc time, 9.87% compilation time: 16% of which was recompilation)
NonconvexTOBS.TOBSResult{Vector{Float64}, Float64, Float64}([1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 … 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 49.638778969559105, 0.020322295084687463)
The TOBS algorithm iterates:
- Linearize objective and constraints around current design
- Solve binary subproblem with Cbc.jl
- Determine which variables to flip (0→1 or 1→0)
- Update design and repeat
Results
@show obj(r.minimizer)
@show constr(r.minimizer)obj(r.minimizer) = 49.638778969559105
constr(r.minimizer) = -1.1270415267716771e-8
-1.1270415267716771e-8
The final design should be nearly binary (values close to 0 or 1) with satisfied volume constraint. TOBS typically produces crisp black/white designs without intermediate densities.
Visualization
topology = r.minimizer
fig = visualize(problem; static=true, topology=topology)
display_app(fig)The visualization shows the final binary structure — TOBS naturally produces clean 0/1 designs without the gray regions common in SIMP results.