Optimization
Overview
Optimization is the search for the best inputs: the radius that makes a part strongest, the shape that wastes the least material, the parameters that make a simulation match measurements. In Rasmah that search is a program that starts at a guess and walks downhill — using the gradient of the thing you care about to know which way is down.
The key idea is familiar from walking down a hill. If you know the slope under your feet, you take a step downhill, look again, and repeat. A gradient is the slope generalized to many inputs at once: for each parameter it says "if I nudge this one a little, how much does the objective change?" Follow the negative gradient and the objective decreases, step by step, until you stand at a minimum where the slope is zero in every direction.
Unconstrained optimization
An unconstrained problem has no rules — any input is allowed, and you just want the smallest value. Rasmah bundles the objective and its gradient into an ObjectiveFunction and hands it to minimize:
Rasmah.Optimization.ObjectiveFunction — Type
ObjectiveFunction(f, g)A bundle of an objective value f and its analytic gradient g, passed to minimize.
Fields
f: objective functionf(θ) -> value.g: gradientg(θ) -> vector.
Rasmah.Optimization.minimize — Function
minimize(obj, θ0; method=:lbfgs, iterations=nothing, store_trace=false, g_tol=nothing, …)Minimize an ObjectiveFunction (a value plus its analytic gradient) starting from θ0 with the native L-BFGS (:lbfgs), BFGS (:bfgs), or adaptive regularization (:arc) method.
Theory
A differentiable objective $f(\theta)$ is minimized where its gradient vanishes, $\nabla f = 0$. The quasi-Newton methods here build a successive approximation $B_k$ of the inverse Hessian and take descent steps $\theta_{k+1} = \theta_k - B_k \nabla f(\theta_k)$, so each iterate moves downhill without ever forming $\nabla^2 f$. L-BFGS keeps only the last mem curvature pairs $(s, y)$, giving a bounded-memory approximation.
Arguments
obj: anObjectiveFunctionbundlingf(θ)andg(θ).θ0: the starting point.
Keyword options
method = :lbfgs::lbfgs,:bfgs, or:arc.iterations = nothing: the iteration budget (default300).store_trace = false: record the iterate history intrace.g_tol = nothing: the gradient-norm tolerance (default1e-8).callback = nothing: called once per iterate (including the starting point,pseudo_iteration = 0) with a named tuple(; x, f_x, g_x, pseudo_iteration); returntrueto stop early.linesearch = :morethuente: the step-size rule.mem = 10: the L-BFGS memory (number of curvature pairs).
Returns
A named tuple (minimizer, minimum, iterations, trace).
Example
julia> obj = ObjectiveFunction(θ -> (θ[1]-3)^2 + (θ[2]-4)^2, θ -> [2(θ[1]-3), 2(θ[2]-4)]);
julia> minimize(obj, [0.0, 0.0]).minimum == 0.0
trueSee also: solve_nonlinear, brent_minimize.
f = θ -> (θ[1] - 3)^2 + (θ[2] + 1)^2
g = θ -> [2 * (θ[1] - 3), 2 * (θ[2] + 1)]
minimize(ObjectiveFunction(f, g), [0.0, 0.0]).minimizer2-element Vector{Float64}:
3.0
-1.0The objective $(\theta_1 - 3)^2 + (\theta_2 + 1)^2$ has one obvious answer: $\theta = [3, -1]$, where it is exactly zero. Quasi-Newton methods like L-BFGS reach that point in a handful of steps by building an approximation of the curvature from the gradients it has seen, which is why they need far fewer evaluations than naive steepest descent.
For a function of a single variable there is a simpler, derivative-free specialist that brackets the minimum and shrinks the bracket until it pinpoints it:
Rasmah.Optimization.brent_minimize — Function
brent_minimize(f, lo, hi; tol, maxiter) -> (x, f(x))Brent's 1-D minimization of f over the bracket [lo, hi] (golden-section search combined with parabolic interpolation), assuming f is unimodal on the interval. Returns the minimizer x and its value f(x).
Example
julia> using Rasmah
julia> brent_minimize(x -> (x - 0.3)^2, 0.0, 1.0)
(0.3, 0.0)brent_minimize(x -> (x - 2)^2, 0.0, 4.0)(2.0, 0.0)Remeshing-aware optimization
Some objectives depend on a mesh that changes as the parameters move — a topology-optimization loop rebuilds its mesh as the design evolves. Ordinary optimizers would keep marching against a stale mesh, so Rasmah offers a loop that re-evaluates geometry and mesh each step, and re-freezes the mesh whenever its quality degrades:
Rasmah.Optimization.remesh_optimize — Function
remesh_optimize(build, θ0; remesh=:on_quality, quality_threshold=0.2, step=0.05, max_iter=50) -> RemeshOptimizationResultOptimize a problem whose geometry must be remeshed along the way. build(θ) returns (mesh_at, obj, grad) for the current parameters; remesh ∈ :local, :on_quality, :every_iteration, :frozen controls when the mesh is rebuilt. Returns a RemeshOptimizationResult.
Rasmah.Optimization.RemeshOptimizationResult — Type
RemeshOptimizationResultThe result of remesh_optimize: the minimizer, the objective_history per iterate, and the number of remeshes performed (remesh_count).
Constrained optimization
Most real problems have rules: a thickness must stay positive, two lengths must sum to a fixed value, a probability vector must sum to one. These are constraints, and they make the search harder because not every direction is allowed.
Rasmah describes such a problem with the @problem DSL — declare each variable in its domain, write the objective and constraints as plain expressions, and solve with optimize:
Rasmah.Optimization.@problem — Macro
@problem begin
x = bounded(lo, hi) # scalar, lo ≤ x ≤ hi
y = free() # free scalar
z = positive() # z > 0
s = simplex(n) # length-n probability simplex (xᵢ ≥ 0, Σ xᵢ = 1)
M = psd(n) # n×n PSD matrix
v = free(3) # length-3 vector
minimize(obj_expression)
subject_to(lhs == rhs) # equality
subject_to(lhs <= rhs) # inequality
subject_to(lhs >= rhs) # inequality
endBuild an OptimizationProblem from a domain-declaration + expression DSL. Variable declarations use the domain constructors; minimize(...) sets the objective and subject_to(...) adds equality/inequality constraints.
The variable domains spell out the rules:
Rasmah.Optimization.free — Function
free() # scalar
free(n) # length-n vector
free(n, m, …) # n×m×… arrayUnbounded real variable(s) of the given shape.
Rasmah.Optimization.bounded — Function
bounded(lo, hi)Scalar variable constrained to lo ≤ x ≤ hi.
Rasmah.Optimization.positive — Function
positive()Scalar variable constrained to x > 0.
Rasmah.Optimization.simplex — Function
simplex(n)Length-n vector variable constrained to the probability simplex {x : xᵢ ≥ 0, Σᵢ xᵢ = 1}.
Rasmah.Optimization.psd — Function
psd(n)Symmetric positive-semidefinite n×n matrix variable.
Rasmah.optimize — Function
optimize(prob::OptimizationProblem, θ0; algorithm=:auglag, reparam=true, …)Solve a constrained OptimizationProblem (built with @problem) from the starting point θ0.
Theory
A constrained problem is minimized over a feasible set — the points that satisfy every equality, inequality, bound, simplex, and PSD constraint. Rasmah handles the feasible set in two complementary ways. With reparam = true the variable domains are baked into a smooth change of variables (bounds become logistic, positivity becomes exp, a simplex becomes stick-breaking, a PSD cone becomes an unconstrained lower-triangular factor), so a plain unconstrained method can drive the search. For the constraints that cannot be absorbed that way, the solvers build an augmented Lagrangian — the objective plus a penalty on the constraint violations — and solve a sequence of unconstrained subproblems.
Arguments
prob: theOptimizationProblemto solve.θ0: the initial flat parameter vector.
Keyword options
algorithm = :auglag: the solver —:auglag(augmented Lagrangian),:lbfgs,:arc,:mma/:mma_nullspace/:mma_hybrid/:mma_aggregate,:interior_point,:sqp,:bayes,:direct,:cmaes,:nelder_mead,:memetic,:multistart,:hyperband,:asha,:bohb,:dehb.reparam = true: bake bounds/positivity/simplex/PSD into the variable transform.penalty_rule = :lancelot: the augmented-Lagrangian penalty update (:lancelot,:birgin_martinez, or:spectral).psd_form = :prox: the PSD-cone treatment (:proxor:classic).linear_eq/linear_ineq/bounds/psd: extra linear constraints, box bounds, and PSD blocks appended to those implied byprob.
Returns
A solver result with at least minimizer and minimum.
Example
julia> prob = @problem begin
x = bounded(0.0, 1.0)
y = free()
minimize((x - 1)^2 + (y - 2)^2)
subject_to(x + y == 2)
end;
julia> r = optimize(prob, [0.5, 0.5]);
julia> isapprox(r.minimizer[1], 0.5; atol=1e-4) && isapprox(r.minimizer[2], 1.5; atol=1e-4)
trueoptimize(prob::TopOptProblem; algorithm=:mma, volfrac=0.5, …)Run topology optimization. algorithm ∈ :mma, :beso, :tobs. Returns (; x, history) — the per-element densities and the objective trace.
prob = @problem begin
x = bounded(0.0, 1.0)
y = free()
minimize((x - 1)^2 + (y - 2)^2)
subject_to(x + y == 2)
end
optimize(prob, [0.5, 0.5]; algorithm = :auglag).minimizer2-element Vector{Float64}:
0.5000000053422039
1.4999999953806042Here the equality $x + y = 2$ pins the answer to $x = 0.5,\ y = 1.5$ — the closest point on the line to $(1, 2)$. The solver trades several algorithms (:auglag augmented Lagrangian, :mma, :interior_point, :sqp, :bayes, :direct, :cmaes, …) and reparameterizes the domains so that constrained variables become unconstrained ones behind the scenes, which lets the same quasi-Newton machinery do the work.
Next steps
Optimization finds the best parameters. When the problem is instead to find the parameters that satisfy $f(x) = 0$ exactly, that is root finding — see Nonlinear solvers.
