RasmahRasmah

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.minimizeFunction
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

Keyword options

  • method = :lbfgs: :lbfgs, :bfgs, or :arc.
  • iterations = nothing: the iteration budget (default 300).
  • store_trace = false: record the iterate history in trace.
  • g_tol = nothing: the gradient-norm tolerance (default 1e-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); return true to 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
true

See also: solve_nonlinear, brent_minimize.

source
f = θ -> (θ[1] - 3)^2 + (θ[2] + 1)^2
g = θ -> [2 * (θ[1] - 3), 2 * (θ[2] + 1)]
minimize(ObjectiveFunction(f, g), [0.0, 0.0]).minimizer
2-element Vector{Float64}:
  3.0
 -1.0

The 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_minimizeFunction
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)
source
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_optimizeFunction
remesh_optimize(build, θ0; remesh=:on_quality, quality_threshold=0.2, step=0.05, max_iter=50) -> RemeshOptimizationResult

Optimize 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.

source

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.@problemMacro
@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
end

Build 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.

source

The variable domains spell out the rules:

Rasmah.Optimization.freeFunction
free()          # scalar
free(n)         # length-n vector
free(n, m, …)   # n×m×… array

Unbounded real variable(s) of the given shape.

source
Rasmah.optimizeFunction
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: the OptimizationProblem to 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 (:prox or :classic).
  • linear_eq / linear_ineq / bounds / psd: extra linear constraints, box bounds, and PSD blocks appended to those implied by prob.

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)
true
source
optimize(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.

source
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).minimizer
2-element Vector{Float64}:
 0.5000000053422039
 1.4999999953806042

Here 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.