601 lines
19 KiB
Go
601 lines
19 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package optim
|
||
|
||
import (
|
||
"cmp"
|
||
"math"
|
||
"slices"
|
||
|
||
"sourcedock.dev/petrbalvin/tensor/internal/base"
|
||
"sourcedock.dev/petrbalvin/tensor/internal/core"
|
||
"sourcedock.dev/petrbalvin/tensor/linalg"
|
||
)
|
||
|
||
// Two derivative-free global searchers beside the differential
|
||
// evolution in devolution.go, both drawing from the house xoshiro
|
||
// generator so a fixed seed fixes the whole trajectory.
|
||
//
|
||
// CMA-ES (the covariance matrix adaptation evolution strategy) is the
|
||
// choice for smooth continuous landscapes where a local method cannot
|
||
// be trusted to find the right basin: it adapts a full covariance
|
||
// matrix from the successful offspring, which turns it along the
|
||
// valley whatever orientation the valley has. The implementation is a
|
||
// bounded single run of the standard algorithm with the default
|
||
// parameters of N. Hansen, The CMA Evolution Strategy: A Tutorial,
|
||
// arXiv:1604.00772 (2016): rank-one and rank-mu updates, the evolution
|
||
// paths p_sigma and p_c with their tag for the stalled-path case, and
|
||
// the tutorial's equations (48) to (53) for the weights and rates.
|
||
// No restarts: the restart schemes (IPOP and friends) are the
|
||
// caller's loop, and this entry reports one run honestly.
|
||
//
|
||
// Simulated annealing is the choice for landscapes too rough, too
|
||
// discrete-like or too deceptive for covariance adaptation: a random
|
||
// walk that accepts uphill steps with the Metropolis probability and
|
||
// cools the acceptance threshold geometrically. It is a basin finder,
|
||
// not a precision optimiser.
|
||
|
||
// CMAESOptions tunes MinimiseCMAES. Sigma0 ≤ 0 means 0.3 (the
|
||
// tutorial's typical starting step for problems scaled to O(1)),
|
||
// Generations ≤ 0 means 500, Tolerance ≤ 0 means 1e-12, Seed 0 is
|
||
// replaced by 42 as in MinimiseDifferentialEvolution (any other
|
||
// value, negatives included, seeds the xoshiro stream directly).
|
||
//
|
||
// The tolerance ends the run when either the distribution has
|
||
// collapsed or the landscape has gone flat: the largest principal axis
|
||
// of the search distribution, sigma·sqrt(max C_ii), has fallen to
|
||
// Tolerance·max(1, ‖mean‖∞), or the objective spread over one
|
||
// generation has fallen to Tolerance·max(1, |best|). The axis is the
|
||
// diagonal of C, a proxy for the largest eigenvalue that avoids a
|
||
// second decomposition; a run that stops on the spread criterion on a
|
||
// genuinely flat landscape reports what it saw.
|
||
type CMAESOptions struct {
|
||
Sigma0 float64
|
||
Generations int
|
||
Tolerance float64
|
||
Seed int64
|
||
// AllowBudgetExit makes a run that exhausts Generations report its
|
||
// best point instead of an error. The default is false, so a
|
||
// budget stop is never mistaken for a converged answer; the flag
|
||
// mirrors LBFGSOptions.AllowBudgetExit.
|
||
AllowBudgetExit bool
|
||
}
|
||
|
||
// MinimiseCMAES returns the point and value of the best solution f the
|
||
// strategy found. f receives candidate points as rank-1 arrays; a
|
||
// non-finite value or an error is fatal for the run. There are no
|
||
// bounds: CMA-ES is an unconstrained method, and the caller who needs
|
||
// a box should reparametrise (or use the differential-evolution entry,
|
||
// which clamps).
|
||
//
|
||
// Termination, budget and honesty: a run ends when the tolerance
|
||
// criteria above are met (converged) or when the generation budget is
|
||
// spent. Following the house budget policy, an exhausted budget is an
|
||
// error naming the best value reached and the tolerance it fell short
|
||
// of, never a silent answer; AllowBudgetExit is the documented escape
|
||
// hatch.
|
||
func MinimiseCMAES(f func(*core.Array) (float64, error), x0 *core.Array, opts CMAESOptions) (*core.Array, float64, error) {
|
||
const name = "MinimiseCMAES"
|
||
if x0.Dtype() == core.Complex {
|
||
return nil, 0, base.Errf("%s: complex starting points are not supported", name)
|
||
}
|
||
n := x0.Len()
|
||
if n == 0 {
|
||
return nil, 0, base.Errf("%s: the starting point must have at least one element", name)
|
||
}
|
||
sigma := opts.Sigma0
|
||
if sigma <= 0 {
|
||
sigma = 0.3
|
||
}
|
||
generations := opts.Generations
|
||
if generations <= 0 {
|
||
generations = 500
|
||
}
|
||
tol := opts.Tolerance
|
||
if tol <= 0 {
|
||
tol = 1e-12
|
||
}
|
||
seed := opts.Seed
|
||
if seed == 0 {
|
||
seed = 42
|
||
}
|
||
g := core.NewGenerator(seed)
|
||
|
||
// The tutorial's default parameters, equations (48) to (53).
|
||
lambda := 4 + int(3*math.Log(float64(n)))
|
||
mu := lambda / 2
|
||
weights := make([]float64, mu)
|
||
wSum := 0.0
|
||
for i := range mu {
|
||
weights[i] = math.Log(float64(lambda)/2+0.5) - math.Log(float64(i+1))
|
||
wSum += weights[i]
|
||
}
|
||
for i := range mu {
|
||
weights[i] /= wSum
|
||
}
|
||
muEff := 0.0
|
||
for _, w := range weights {
|
||
muEff += w * w
|
||
}
|
||
muEff = 1 / muEff
|
||
cSigma := (muEff + 2) / (float64(n) + muEff + 5)
|
||
dSigma := 1 + 2*math.Max(0, math.Sqrt((muEff-1)/(float64(n)+1))-1) + cSigma
|
||
cC := (4 + muEff/float64(n)) / (4 + float64(n) + 2*muEff/float64(n))
|
||
c1 := 2 / ((float64(n)+1.3)*(float64(n)+1.3) + muEff)
|
||
cMu := math.Min(1-c1, 2*(muEff-2+1/muEff)/((float64(n)+2)*(float64(n)+2)+muEff))
|
||
chiN := math.Sqrt(float64(n)) * (1 - 1/(4*float64(n)) + 1/(21*float64(n)*float64(n)))
|
||
|
||
mean := cloneDense(x0)
|
||
cov := make([]float64, n*n)
|
||
for i := range n {
|
||
cov[i*n+i] = 1
|
||
}
|
||
pathC := make([]float64, n)
|
||
pathS := make([]float64, n)
|
||
|
||
eval := func(p []float64) (float64, error) {
|
||
v, err := f(linalg.ArrayFromFloatsSafe(p, len(p)))
|
||
if err != nil {
|
||
return 0, base.Errf("%s: %w", name, err)
|
||
}
|
||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||
return 0, base.Errf("%s: the objective is non-finite (%g)", name, v)
|
||
}
|
||
return v, nil
|
||
}
|
||
|
||
bestF, bestX := math.Inf(1), make([]float64, n)
|
||
offX := make([]float64, lambda*n)
|
||
offY := make([]float64, lambda*n)
|
||
offF := make([]float64, lambda)
|
||
order := make([]int, lambda)
|
||
yW := make([]float64, n)
|
||
yC := make([]float64, n)
|
||
newCov := make([]float64, n*n)
|
||
// Per-generation scratch, owned by the run: the sampling scale per
|
||
// principal axis, the unit normal drawn per offspring, the
|
||
// eigenvector projection rootInverse accumulates in, the rank-mu
|
||
// outer-product accumulator with its weighted axis vector, and the
|
||
// diagonalisation's own working set. Each is fully overwritten
|
||
// before it is read.
|
||
sd := make([]float64, n)
|
||
z := make([]float64, n)
|
||
yInv := make([]float64, n)
|
||
rankMu := make([]float64, n*n)
|
||
wy := make([]float64, n)
|
||
var eig jacobiScratch
|
||
|
||
for gen := range generations {
|
||
vals, vecs := eig.eigen(cov, n)
|
||
// Sampling scale per principal axis, floored at zero: a
|
||
// numerically degenerate axis contributes nothing rather than
|
||
// a complex square root.
|
||
for i := range n {
|
||
sd[i] = math.Sqrt(math.Max(vals[i], 0))
|
||
}
|
||
worstF := math.Inf(-1)
|
||
bestGen := math.Inf(1)
|
||
for k := range lambda {
|
||
// One unit normal per principal direction: the draw is
|
||
// B·diag(sd)·z, whose covariance is exactly C. Sharing a
|
||
// single scalar across the directions would sample a
|
||
// diagonal distribution scaled by one fixed vector and
|
||
// leave the adapted covariance unused.
|
||
for i := range n {
|
||
z[i] = g.NormalUnit()
|
||
}
|
||
cmaDraw(vecs, sd, z, offY[k*n:k*n+n])
|
||
for i := range n {
|
||
offX[k*n+i] = mean[i] + sigma*offY[k*n+i]
|
||
}
|
||
v, err := eval(offX[k*n : k*n+n])
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
offF[k] = v
|
||
worstF = math.Max(worstF, v)
|
||
bestGen = math.Min(bestGen, v)
|
||
}
|
||
for k := range lambda {
|
||
order[k] = k
|
||
}
|
||
slices.SortStableFunc(order, func(a, b int) int { return cmp.Compare(offF[a], offF[b]) })
|
||
if offF[order[0]] < bestF {
|
||
bestF = offF[order[0]]
|
||
copy(bestX, offX[order[0]*n:(order[0]+1)*n])
|
||
}
|
||
clear(yW)
|
||
for k := range mu {
|
||
w := weights[k]
|
||
for i := range n {
|
||
yW[i] += w * offY[order[k]*n+i]
|
||
}
|
||
}
|
||
for i := range n {
|
||
mean[i] += sigma * yW[i]
|
||
}
|
||
|
||
// Conjugate evolution path: C^{-1/2} y_w through the
|
||
// eigendecomposition, then the sigma update from its length.
|
||
rootInverse(yC, yW, vals, vecs, yInv, n)
|
||
psNorm := 0.0
|
||
for i := range n {
|
||
pathS[i] = (1-cSigma)*pathS[i] + math.Sqrt(cSigma*(2-cSigma)*muEff)*yC[i]
|
||
psNorm += pathS[i] * pathS[i]
|
||
}
|
||
psNorm = math.Sqrt(psNorm)
|
||
sigma *= math.Exp((cSigma / dSigma) * (psNorm/chiN - 1))
|
||
hs := 0.0
|
||
if psNorm/math.Sqrt(1-math.Pow(1-cSigma, 2*float64(gen+1))) < (1.4+2/(float64(n)+1))*chiN {
|
||
hs = 1
|
||
}
|
||
pcNormScale := math.Sqrt(cC * (2 - cC) * muEff)
|
||
for i := range n {
|
||
pathC[i] = (1-cC)*pathC[i] + hs*pcNormScale*yW[i]
|
||
}
|
||
// The rank-one and rank-mu updates; delta(hs) keeps the
|
||
// covariance from growing along p_c across a stall of the
|
||
// conjugate path. The rank-mu sum accumulates as contiguous
|
||
// outer products, one per selected offspring walked in order:
|
||
// every cell still sums (w·yᵢ)·yⱼ over ascending k, so the
|
||
// bits are the per-cell walk's and the inner loop stays on
|
||
// unit stride.
|
||
clear(rankMu)
|
||
for k := range mu {
|
||
base := order[k] * n
|
||
w := weights[k]
|
||
for i := range n {
|
||
wy[i] = w * offY[base+i]
|
||
}
|
||
for i := range n {
|
||
wi := wy[i]
|
||
row := rankMu[i*n : i*n+n]
|
||
yk := offY[base : base+n]
|
||
for j := range n {
|
||
row[j] += wi * yk[j]
|
||
}
|
||
}
|
||
}
|
||
factor := 1 + c1*(1-hs) - c1 - cMu
|
||
for i := range n {
|
||
ci := c1 * pathC[i]
|
||
off := i * n
|
||
for j := range n {
|
||
newCov[off+j] = factor*cov[off+j] + ci*pathC[j] + cMu*rankMu[off+j]
|
||
}
|
||
}
|
||
for i := range n {
|
||
for j := i + 1; j < n; j++ {
|
||
avg := (newCov[i*n+j] + newCov[j*n+i]) / 2
|
||
newCov[i*n+j], newCov[j*n+i] = avg, avg
|
||
}
|
||
}
|
||
copy(cov, newCov)
|
||
|
||
axis := 0.0
|
||
for i := range n {
|
||
axis = math.Max(axis, sigma*math.Sqrt(math.Max(cov[i*n+i], 0)))
|
||
}
|
||
if math.IsNaN(axis) || sigma > 1e12 {
|
||
return nil, 0, base.Errf("%s: the search diverged (sigma %g, largest axis %g) at f = %g", name, sigma, axis, bestF)
|
||
}
|
||
if axis <= tol*math.Max(1, maxAbs(mean)) || worstF-bestGen <= tol*math.Max(1, math.Abs(bestF)) {
|
||
out, fv := packResult(bestX, bestF)
|
||
return out, fv, nil
|
||
}
|
||
}
|
||
if !opts.AllowBudgetExit {
|
||
return nil, 0, base.Errf("%s: the generation budget of %d ran out at f = %g, above the tolerance %g", name, generations, bestF, tol)
|
||
}
|
||
out, fv := packResult(bestX, bestF)
|
||
return out, fv, nil
|
||
}
|
||
|
||
// rootInverse writes C^{-1/2} y into dst through the eigendecomposition
|
||
// the caller already paid for: B·diag(1/sqrt(d))·Bᵀ·y with the
|
||
// eigenvalues floored away from zero so a numerically flat direction
|
||
// cannot divide by nothing. tmp is scratch of length at least n, fully
|
||
// overwritten before it is read.
|
||
func rootInverse(dst, y, vals, vecs, tmp []float64, n int) {
|
||
scale := 0.0
|
||
for i := range n {
|
||
scale = math.Max(scale, vals[i])
|
||
}
|
||
floor := 1e-20 * math.Max(1, scale)
|
||
for j := range n { // tmp = Bᵀ y
|
||
s := 0.0
|
||
for i := range n {
|
||
s += vecs[j*n+i] * y[i]
|
||
}
|
||
tmp[j] = s / math.Sqrt(math.Max(vals[j], floor))
|
||
}
|
||
clear(dst)
|
||
for j := range n { // dst = B tmp
|
||
c := tmp[j]
|
||
if c == 0 {
|
||
continue
|
||
}
|
||
for i := range n {
|
||
dst[i] += vecs[j*n+i] * c
|
||
}
|
||
}
|
||
}
|
||
|
||
// eigenPair is one eigenvalue with the column its eigenvector occupies
|
||
// in the accumulator the diagonalisation carries.
|
||
type eigenPair struct {
|
||
val float64
|
||
index int
|
||
}
|
||
|
||
// jacobiScratch is one diagonalisation's working set: the cyclically
|
||
// rotated copy of the matrix, the eigenvector accumulator, the
|
||
// value/index pairs the descending sort walks and the two blocks handed
|
||
// back. The driver keeps one instance for the whole run, so the
|
||
// per-generation decomposition allocates nothing; every buffer is fully
|
||
// overwritten before it is read.
|
||
type jacobiScratch struct {
|
||
work []float64
|
||
acc []float64
|
||
pairs []eigenPair
|
||
vals []float64
|
||
sorted []float64
|
||
evecs []float64
|
||
}
|
||
|
||
// jacobiEigen diagonalises the symmetric row-major n×n matrix into
|
||
// freshly allocated blocks, the allocating entry over
|
||
// (*jacobiScratch).eigen.
|
||
func jacobiEigen(a []float64, n int) (vals, vecs []float64) {
|
||
var s jacobiScratch
|
||
return s.eigen(a, n)
|
||
}
|
||
|
||
// eigen diagonalises the symmetric row-major n×n matrix by cyclic
|
||
// Jacobi rotations: the eigenvalues come back in descending order and
|
||
// vecs[j*n+i] is component i of the eigenvector that belongs to
|
||
// vals[j]. The sweeps stop once the off-diagonal mass has fallen to
|
||
// the working precision of the matrix's own Frobenius norm. Both
|
||
// returned blocks belong to the scratch and stay valid until its next
|
||
// call.
|
||
func (s *jacobiScratch) eigen(a []float64, n int) (vals, vecs []float64) {
|
||
if cap(s.work) < n*n {
|
||
s.work = make([]float64, n*n)
|
||
}
|
||
work := s.work[:n*n]
|
||
copy(work, a)
|
||
if cap(s.acc) < n*n {
|
||
s.acc = make([]float64, n*n)
|
||
}
|
||
acc := s.acc[:n*n]
|
||
clear(acc)
|
||
for i := range n {
|
||
acc[i*n+i] = 1
|
||
}
|
||
frob := 0.0
|
||
for _, v := range work {
|
||
frob += v * v
|
||
}
|
||
const maxSweeps = 60
|
||
for range maxSweeps {
|
||
off := 0.0
|
||
for i := range n {
|
||
for j := i + 1; j < n; j++ {
|
||
off += work[i*n+j] * work[i*n+j]
|
||
}
|
||
}
|
||
if off <= 1e-30*math.Max(frob, 1) {
|
||
break
|
||
}
|
||
for p := range n {
|
||
for q := p + 1; q < n; q++ {
|
||
apq := work[p*n+q]
|
||
if apq == 0 {
|
||
continue
|
||
}
|
||
theta := (work[q*n+q] - work[p*n+p]) / (2 * apq)
|
||
t := 1 / (math.Abs(theta) + math.Sqrt(theta*theta+1))
|
||
if theta < 0 {
|
||
t = -t
|
||
}
|
||
c := 1 / math.Sqrt(t*t+1)
|
||
s := t * c
|
||
for k := range n {
|
||
akp, akq := work[k*n+p], work[k*n+q]
|
||
work[k*n+p] = c*akp - s*akq
|
||
work[k*n+q] = s*akp + c*akq
|
||
}
|
||
for k := range n {
|
||
apk, aqk := work[p*n+k], work[q*n+k]
|
||
work[p*n+k] = c*apk - s*aqk
|
||
work[q*n+k] = s*apk + c*aqk
|
||
}
|
||
work[p*n+q], work[q*n+p] = 0, 0
|
||
for k := range n {
|
||
vkp, vkq := acc[k*n+p], acc[k*n+q]
|
||
acc[k*n+p] = c*vkp - s*vkq
|
||
acc[k*n+q] = s*vkp + c*vkq
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if cap(s.vals) < n {
|
||
s.vals = make([]float64, n)
|
||
}
|
||
vals = s.vals[:n]
|
||
for i := range n {
|
||
vals[i] = work[i*n+i]
|
||
}
|
||
// Sort the pairs descending by value. The accumulator carries
|
||
// eigenvector k in its column k, and the documented layout here is
|
||
// row j for vector j, so the extraction transposes.
|
||
if cap(s.pairs) < n {
|
||
s.pairs = make([]eigenPair, n)
|
||
}
|
||
pairs := s.pairs[:n]
|
||
for i := range n {
|
||
pairs[i] = eigenPair{vals[i], i}
|
||
}
|
||
slices.SortFunc(pairs, func(a, b eigenPair) int { return cmp.Compare(b.val, a.val) })
|
||
if cap(s.sorted) < n {
|
||
s.sorted = make([]float64, n)
|
||
}
|
||
if cap(s.evecs) < n*n {
|
||
s.evecs = make([]float64, n*n)
|
||
}
|
||
sortedVals := s.sorted[:n]
|
||
sortedVecs := s.evecs[:n*n]
|
||
for j, pr := range pairs {
|
||
sortedVals[j] = pr.val
|
||
for i := range n {
|
||
sortedVecs[j*n+i] = acc[i*n+pr.index]
|
||
}
|
||
}
|
||
return sortedVals, sortedVecs
|
||
}
|
||
|
||
// SimulatedAnnealingOptions tunes MinimiseSimulatedAnnealing.
|
||
// Steps ≤ 0 means 20000 proposals, Temperature0 ≤ 0 means 1,
|
||
// CoolingRate ≤ 0 means 0.9995 (the geometric factor per proposal),
|
||
// StepScale ≤ 0 means 0.1 (the relative Gaussian proposal scale),
|
||
// Tolerance ≤ 0 means 1e-6, Seed 0 is replaced by 42 as in
|
||
// MinimiseDifferentialEvolution.
|
||
//
|
||
// The schedule is the algorithm: the temperature runs from
|
||
// Temperature0 down by the factor CoolingRate at every proposal, so
|
||
// the chain freezes exponentially and the tail of the schedule is a
|
||
// local polish. Tolerance is the convergence test on that tail: the
|
||
// best value may not improve by more than Tolerance·max(1, |best|)
|
||
// over the final quarter of the schedule. A run that is still
|
||
// improving at the end of the schedule has not converged, and the
|
||
// budget refusal says so with both figures, exactly as the local
|
||
// solvers refuse an unfinished run; AllowBudgetExit is the escape
|
||
// hatch that reports the best point anyway.
|
||
type SimulatedAnnealingOptions struct {
|
||
Steps int
|
||
Temperature0 float64
|
||
CoolingRate float64
|
||
StepScale float64
|
||
Seed int64
|
||
Tolerance float64
|
||
// AllowBudgetExit makes a run whose schedule ended while the best
|
||
// value was still improving report its best point instead of an
|
||
// error. The flag mirrors LBFGSOptions.AllowBudgetExit.
|
||
AllowBudgetExit bool
|
||
}
|
||
|
||
// MinimiseSimulatedAnnealing returns the best point and value the
|
||
// chain found: a Metropolis walk over the landscape with a Gaussian
|
||
// proposal of the given relative scale (coordinate i moves by
|
||
// StepScale·max(1, |x_i|) standard normals) and the geometric cooling
|
||
// schedule above. f receives candidate points as rank-1 arrays; a
|
||
// non-finite value or an error is fatal for the run. Simulated
|
||
// annealing finds basins, not minima to full precision: run
|
||
// MinimiseLBFGS from the returned point when a polished answer is
|
||
// wanted, which is the standard composition.
|
||
func MinimiseSimulatedAnnealing(f func(*core.Array) (float64, error), x0 *core.Array, opts SimulatedAnnealingOptions) (*core.Array, float64, error) {
|
||
const name = "MinimiseSimulatedAnnealing"
|
||
if x0.Dtype() == core.Complex {
|
||
return nil, 0, base.Errf("%s: complex starting points are not supported", name)
|
||
}
|
||
n := x0.Len()
|
||
if n == 0 {
|
||
return nil, 0, base.Errf("%s: the starting point must have at least one element", name)
|
||
}
|
||
steps := opts.Steps
|
||
if steps <= 0 {
|
||
steps = 20000
|
||
}
|
||
t0 := opts.Temperature0
|
||
if t0 <= 0 {
|
||
t0 = 1
|
||
}
|
||
cooling := opts.CoolingRate
|
||
if cooling > 1 {
|
||
return nil, 0, base.Errf("%s: the cooling rate %g would heat the chain; want a factor in (0, 1]", name, cooling)
|
||
}
|
||
if cooling <= 0 {
|
||
cooling = 0.9995
|
||
}
|
||
scale := opts.StepScale
|
||
if scale <= 0 {
|
||
scale = 0.1
|
||
}
|
||
tol := opts.Tolerance
|
||
if tol <= 0 {
|
||
tol = 1e-6
|
||
}
|
||
seed := opts.Seed
|
||
if seed == 0 {
|
||
seed = 42
|
||
}
|
||
g := core.NewGenerator(seed)
|
||
|
||
x := cloneDense(x0)
|
||
cur, err := f(linalg.ArrayFromFloatsSafe(x, n))
|
||
if err != nil {
|
||
return nil, 0, base.Errf("%s: %w", name, err)
|
||
}
|
||
if math.IsNaN(cur) || math.IsInf(cur, 0) {
|
||
return nil, 0, base.Errf("%s: the objective is non-finite (%g) at the start", name, cur)
|
||
}
|
||
bestF, bestX := cur, slices.Clone(x)
|
||
proposal := make([]float64, n)
|
||
quarterF := math.Inf(1)
|
||
quarterAt := 3 * steps / 4
|
||
|
||
for k := range steps {
|
||
temperature := t0 * math.Pow(cooling, float64(k))
|
||
for i := range n {
|
||
proposal[i] = x[i] + scale*math.Max(1, math.Abs(x[i]))*g.NormalUnit()
|
||
}
|
||
fv, ferr := f(linalg.ArrayFromFloatsSafe(proposal, n))
|
||
if ferr != nil {
|
||
return nil, 0, base.Errf("%s: %w", name, ferr)
|
||
}
|
||
if math.IsNaN(fv) || math.IsInf(fv, 0) {
|
||
return nil, 0, base.Errf("%s: the objective is non-finite (%g) at proposal %d", name, fv, k+1)
|
||
}
|
||
delta := fv - cur
|
||
if delta <= 0 || g.Unit() < math.Exp(-delta/temperature) {
|
||
copy(x, proposal)
|
||
cur = fv
|
||
}
|
||
if cur < bestF {
|
||
bestF = cur
|
||
copy(bestX, x)
|
||
}
|
||
if k == quarterAt {
|
||
quarterF = bestF
|
||
}
|
||
}
|
||
// The schedule ended: convergence is the frozen tail, judged by
|
||
// the improvement the final quarter still bought.
|
||
if quarterF-bestF > tol*math.Max(1, math.Abs(bestF)) && !opts.AllowBudgetExit {
|
||
return nil, 0, base.Errf("%s: the schedule of %d steps ended with the best value still improving (%g to %g); raise Steps or set AllowBudgetExit",
|
||
name, steps, quarterF, bestF)
|
||
}
|
||
out, fv := packResult(bestX, bestF)
|
||
return out, fv, nil
|
||
}
|
||
|
||
// cmaDraw fills y with B·diag(sd)·z: one unit normal per principal
|
||
// direction transformed into the covariance's own axes, the draw the
|
||
// tutorial's sampling equation defines, whose covariance is the
|
||
// adapted C itself. The walk goes one principal direction at a time so
|
||
// the eigenvector rows are read on unit stride; the per-element
|
||
// grouping (v·sdⱼ)·zⱼ and the ascending j order are the i-outer walk's,
|
||
// so the draw is bit for bit the same.
|
||
func cmaDraw(vecs, sd, z, y []float64) {
|
||
clear(y)
|
||
n := len(y)
|
||
for j := range z {
|
||
sdJ, zJ := sd[j], z[j]
|
||
row := vecs[j*n : j*n+n]
|
||
for i := range n {
|
||
y[i] += row[i] * sdJ * zJ
|
||
}
|
||
}
|
||
}
|