/* LA: linear algebra C++ interface library Copyright (C) 2026 Jiri Pittner or This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "optimize.h" namespace LA { template T testproblem::funcgrad(const NRVec &x, NRVec *grad) const { int n=x.size(); T r= n*(n-1)/2; for(int i=0; isize()!=x.size()) laerror("grad size mismatch"); grad->copyonwrite(); //I forgot this initially for(int k=0; k static T cubic_interpolate(const T a, const T fa, const T da, const T b, const T fb, const T db) { T lo= a=lo+safe && r<=hi-safe)) r=(a+b)/2; //also catches NaN return r; } template T linesearch_wolfe(optevaluator &e, NRVec &x, T &f, NRVec &g, const NRVec &p, const T alpha1, const linesearch_hyperparameters &lp) { const T c1=lp.my_c1; const T c2=lp.my_c2; const int verbose=lp.my_verbose; const int maxiter= std::min(lp.my_maxlinesearch,e.remaining()); const int n=x.size(); const NRVec x0(x); const T f0=f; const T d0= g*p; if(d0>=0) {if(verbose) std::cout <<"linesearch: not a descent direction\n"; return 0;} const T alphamax=lp.my_maxsteplength/p.norm(); T alpha=std::min(alpha1,alphamax); //data of trial points; vectors are always freshly allocated, so that assignments (which share data) are safe T alpha0=0, f_alpha0=f0, d_alpha0=d0; NRVec x_alpha0(x0), g_alpha0(g); //previous trial point T alo=0,flo=0,dlo=0,ahi=0,fhi=0,dhi=0; NRVec xlo,glo; //bracketing interval for the zoom phase, lo is the best point bool bracketed=false; int it; //bracketing phase for(it=0; it xt = x0 + p*alpha; NRVec gt(n); T ft=e(xt,>); T dt= gt*p; if(verbose>1) std::cout <<"linesearch bracket "< static bool QN_update_H(NRSMat &H, bool &Hinit, const NRVec &s, const NRVec &y, const BFGS_hyperparameters &hp) { const T sy = s*y; const T snorm=s.norm(); const T ynorm=y.norm(); const bool curvature = sy > hp.my_sy_thr*snorm*ynorm; if(Hinit && curvature) {H = (T)(sy/(ynorm*ynorm)); Hinit=false;} switch(hp.my_update) { case QN_BFGS: //H+ = (I - rho s y^T) H (I - rho y s^T) + rho s s^T = H + s u^T + u s^T, rho=1/(y^T s), u = (rho^2 y^T H y + rho)/2 s - rho Hy { if(!curvature) return false; T rho = 1/sy; NRVec Hy = H*y; NRVec u = s*((rho*rho*(y*Hy) + rho)/2); u.axpy(-rho,Hy); H.rank2update((T)1,s,u); } break; case QN_DFP: //H+ = H - Hy (Hy)^T/(y^T H y) + s s^T/(y^T s) { if(!curvature) return false; NRVec Hy = H*y; T yHy = y*Hy; if(yHy<=0) return false; H.rank2update(1/(2*sy),s,s); H.rank2update(-1/(2*yHy),Hy,Hy); } break; case QN_SR1: //H+ = H + w w^T/(w^T y), w = s - Hy; H may become indefinite { NRVec w = H*y; w *= (T)-1; w += s; T wy = w*y; if(std::abs(wy) <= hp.my_sr1_thr*w.norm()*ynorm) return false; H.rank2update(1/(2*wy),w,w); } break; default: laerror("unknown quasi-Newton update"); } Hinit=false; return true; } template bool optstate::stop(const T ff, const T gn) { f=ff; gnorm=gn; if(hp.my_verbose) { std::cout < optresult optstate::finish(hyperparameters *achieved) const { if(hp.my_verbose) { std::cout <::epsilon()*std::abs(t1)); //nonzero to guarantee termination while(!phi.exhausted()) { T mid=(lo+hi)/2; T tol1=tol*std::abs(xm)+tolabs; T tol2=2*tol1; if(std::abs(xm-mid) <= tol2-(hi-lo)/2) break; //converged bool goldenstep=true; if(std::abs(dprev)>tol1) //try parabolic interpolation through xm, w, v { T r=(xm-w)*(fx-fv); T q=(xm-v)*(fx-fw); T p=(xm-v)*q-(xm-w)*r; q=2*(q-r); if(q>0) p= -p; else q= -q; if(std::abs(p)q*(lo-xm) && pxm ? tol1 : -tol1; goldenstep=false; } } if(goldenstep) {dprev= xm>=mid ? lo-xm : hi-xm; d=cgold*dprev;} T t= std::abs(d)>=tol1 ? xm+d : xm+(d>0 ? tol1 : -tol1); T ft=phi(t); if(ft<=fx) { if(t>=xm) lo=xm; else hi=xm; v=w; fv=fw; w=xm; fw=fx; xm=t; fx=ft; } else { if(t1) std::cout <<"linemin t= "< optresult linesearch_minimize(const char *name, linesearch_direction &dir, const OptProblem *o, NRVec &x, const linesearch_hyperparameters &hp, hyperparameters *achieved) { const int verbose=hp.my_verbose; optevaluator e(o,hp.my_maxfunc); optstate st(name,hp,e); const int n=x.size(); NRVec g(n); T f=e(x,&g); T alpha=0, dphi=0; //of the previous step for(;; ++st.iter) { if(st.stop(f,g.norm())) break; NRVec p = dir.direction(g); bool sd=dir.steepest(); //steepest descent step if(p*g >= 0) { if(dir.indefinite()) {if(verbose) std::cout <0) alpha1=alpha*dphi/dphi_new; else alpha1= sd ? 1/p.norm() : (T)1; dphi=dphi_new; NRVec xold(x), gold(g); T fold=f; alpha=linesearch_wolfe(e,x,f,g,p,alpha1,hp); if(alpha==0) { if(e.exhausted()) continue; //reported by stop() if(sd) {if(verbose) std::cout < s = x-xold; NRVec y = g-gold; if(verbose>1) std::cout < class QN_direction : public linesearch_direction { public: const BFGS_hyperparameters &hp; NRSMat H; bool Hinit; //H is the initial guess, rescale it after the first step QN_direction(const BFGS_hyperparameters &h, const int n) : hp(h), H(n), Hinit(true) {H=1.;}; NRVec direction(const NRVec &g) {NRVec p = H*g; p *= (T)-1; return p;} bool update(const NRVec &s, const NRVec &y, const NRVec &p, const NRVec &g) {return QN_update_H(H,Hinit,s,y,hp);} void reset() {H=1.; Hinit=true;} bool steepest() const {return Hinit;} bool indefinite() const {return hp.my_update==QN_SR1;} }; template optresult BFGS_Optimizer::minimize(const OptProblem *o, NRVec &x, const hyperparameters *par, hyperparameters *achieved) { static const char *names[]={"BFGS","DFP","SR1"}; BFGS_hyperparameters hp; if(par) hp.setfrom(*par); if(hp.my_saddle_order) laerror("quasi-Newton with line search cannot search for saddle points"); QN_direction dir(hp,x.size()); return linesearch_minimize(names[hp.my_update],dir,o,x,hp,achieved); } //L-BFGS: last m pairs s = x_{k+1}-x_k, y = g_{k+1}-g_k in a ring buffer template class LBFGS_direction : public linesearch_direction { public: const LBFGS_hyperparameters &hp; int m; //capacity int count; //no. of pairs stored int newest; //index of the newest pair NRVec > S,Y; NRVec rho; //1/(s.y) T gamma; //initial inverse Hessian H0 = gamma I, gamma = s.y/y.y of the newest pair (Nocedal & Wright eq. 7.20) LBFGS_direction(const LBFGS_hyperparameters &h) : hp(h), m(h.my_memory), count(0), newest(-1), S(m), Y(m), rho(m), gamma(1) {}; void push(const NRVec &s, const NRVec &y, const T sy) {newest=(newest+1)%m; S[newest]=s; Y[newest]=y; rho[newest]=1/sy; gamma=sy/(y*y); if(count direction(const NRVec &g); bool update(const NRVec &s, const NRVec &y, const NRVec &p, const NRVec &g) { T sy=s*y; if(sy <= hp.my_sy_thr*s.norm()*y.norm()) return false; //curvature condition violated numerically push(s,y,sy); return true; } void reset() {count=0; newest= -1; gamma=1;} bool steepest() const {return count==0;} }; //returns -H g by the two-loop recursion (Nocedal & Wright alg. 7.4) template NRVec LBFGS_direction::direction(const NRVec &g) { NRVec q(g); NRVec alpha(count); int k=newest; for(int i=0; i=0; --i) //oldest to newest { T beta = rho[k]*(Y[k]*q); q.axpy(alpha[i]-beta,S[k]); k=(k+1)%m; } q *= (T)-1; return q; } template optresult LBFGS_Optimizer::minimize(const OptProblem *o, NRVec &x, const hyperparameters *par, hyperparameters *achieved) { LBFGS_hyperparameters hp; if(par) hp.setfrom(*par); if(hp.my_saddle_order) laerror("L-BFGS cannot search for saddle points"); if(hp.my_memory<1) laerror("L-BFGS needs memory >= 1"); LBFGS_direction dir(hp); return linesearch_minimize("L-BFGS",dir,o,x,hp,achieved); } //nonlinear CG: p_{k+1} = -g_{k+1} + beta p_k template class CG_direction : public linesearch_direction { public: const CG_hyperparameters &hp; int restart_interval; int count; //no. of steps since the last restart bool restart; //next direction is steepest descent T beta; T gg; //g.g of the gradient of the last direction NRVec d; //last search direction CG_direction(const CG_hyperparameters &h, const int n) : hp(h), restart_interval(h.my_restart>0 ? h.my_restart : n), count(0), restart(true), beta(0), gg(0) {}; NRVec direction(const NRVec &g) {NRVec p = g*(T)-1; if(!restart) p.axpy(beta,d); gg=g*g; return p;} bool update(const NRVec &s, const NRVec &y, const NRVec &p, const NRVec &g); void reset() {restart=true; count=0;} bool steepest() const {return restart;} bool alpha_from_previous() const {return true;} }; template bool CG_direction::update(const NRVec &s, const NRVec &y, const NRVec &p, const NRVec &g) { d=p; T gg1=g*g; T gy=g*y; T dy=p*y; restart= ++count>=restart_interval || (hp.my_powell_thr>0 && std::abs(gg1-gy) >= hp.my_powell_thr*gg1); //g_{k+1}.g_k = gg1-gy bool curvature= dy>0; if(!restart) switch(hp.my_beta) { case CG_FR: beta=gg1/gg; break; case CG_PR: beta=std::max((T)0,gy/gg); break; case CG_HS: if(curvature) beta=std::max((T)0,gy/dy); else restart=true; break; case CG_DY: if(curvature) beta=gg1/dy; else restart=true; break; case CG_HZ: if(curvature) beta=std::max((gy-2*(p*g)*(y*y)/dy)/dy, -1/(p.norm()*std::min((T)0.01,std::sqrt(gg)))); else restart=true; break; default: laerror("unknown CG beta formula"); } if(restart) { count=0; if(hp.my_verbose>1) std::cout <<"CG restart\n"; } return true; } template optresult CG_Optimizer::minimize(const OptProblem *o, NRVec &x, const hyperparameters *par, hyperparameters *achieved) { static const char *names[]={"CG-FR","CG-PR+","CG-HS+","CG-DY","CG-HZ"}; CG_hyperparameters hp; if(par) hp.setfrom(*par); if(hp.my_saddle_order) laerror("CG cannot search for saddle points"); CG_direction dir(hp,x.size()); return linesearch_minimize(names[hp.my_beta],dir,o,x,hp,achieved); } //simplex of n+1 vertices with their function values template class NM_simplex { public: int n; NRVec > v; //vertices NRVec f; //function values NRVec sum; //sum of the vertices int best, second, worst; //indices of the lowest, second highest and highest function value NM_simplex(const NRVec &x0, const T h, optevaluator &e); void order(); void sumup() {sum=v[0]; for(int i=1; i<=n; ++i) sum+=v[i];} void replace(const int i, const NRVec &x, const T fx) {sum-=v[i]; sum+=x; v[i]=x; f[i]=fx;} NRVec centroid() const {NRVec c(sum); c-=v[worst]; c/=(T)n; return c;} //of all vertices except the worst one NRVec point(const NRVec &c, const T t) const {NRVec x = c*(1+t); x.axpy(-t,v[worst]); return x;} //c + t*(c - x_worst) T diameter() const; void shrink(const T sigma, optevaluator &e); }; //initial simplex x0, x0 + h e_i template NM_simplex::NM_simplex(const NRVec &x0, const T h, optevaluator &e) : n(x0.size()), v(n+1), f(n+1), best(0), second(0), worst(0) { v[0]=x0; f[0]=e(x0); for(int i=1; i<=n; ++i) { v[i]=x0; v[i].copyonwrite(); v[i][i-1] += h; f[i]=e(v[i]); } sumup(); } template void NM_simplex::order() { best=worst=0; for(int i=1; i<=n; ++i) { if(f[i]=f[worst]) worst=i; } second=best; for(int i=0; i<=n; ++i) if(i!=worst && f[i]>=f[second]) second=i; } template T NM_simplex::diameter() const { T d=0; for(int i=0; i<=n; ++i) if(i!=best) { NRVec dx = v[i]-v[best]; d=std::max(d,dx.norm()); } return d; } //all vertices move towards the best one, as far as the evaluation budget allows template void NM_simplex::shrink(const T sigma, optevaluator &e) { for(int i=0; i<=n; ++i) if(i!=best) { if(e.exhausted()) break; NRVec x = v[best]*(1-sigma); x.axpy(sigma,v[i]); v[i]=x; f[i]=e(x); } sumup(); } template optresult NelderMead_Optimizer::minimize(const OptProblem *o, NRVec &x, const hyperparameters *par, hyperparameters *achieved) { const char *name="Nelder-Mead"; NelderMead_hyperparameters hp; if(par) hp.setfrom(*par); if(hp.my_saddle_order) laerror("Nelder-Mead cannot search for saddle points"); const int verbose=hp.my_verbose; const int n=x.size(); T alpha=hp.my_alpha, gamma=hp.my_gamma, rho=hp.my_rho, sigma=hp.my_sigma; if(hp.my_adaptive) {alpha=1; gamma=1+(T)2/n; rho=0.75-(T)0.5/n; sigma=1-(T)1/n;} optevaluator e(o,hp.my_maxfunc); optstate st(name,hp,e); NM_simplex s(x,hp.my_initstep,e); const T huge=std::numeric_limits::max(); for(;; ++st.iter) { s.order(); T spread=s.f[s.worst]-s.f[s.best]; st.step(spread, spread c=s.centroid(); NRVec xr=s.point(c,alpha); T fr=e(xr); if(fr xe=s.point(c,alpha*gamma); T fe=e(xe); if(fe xc=s.point(c, outside ? alpha*rho : -rho); T fc=e(xc); if(outside ? fc<=fr : fc1) std::cout < optresult Powell_Optimizer::minimize(const OptProblem *o, NRVec &x, const hyperparameters *par, hyperparameters *achieved) { const char *name="Powell"; Powell_hyperparameters hp; if(par) hp.setfrom(*par); if(hp.my_saddle_order) laerror("Powell cannot search for saddle points"); const int verbose=hp.my_verbose; const T abstol=0.01*hp.my_x_thr; //line minima have to be more precise than the requested accuracy in x const int n=x.size(); optevaluator e(o,hp.my_maxfunc); optstate st(name,hp,e); T f=e(x); NRVec > u(n); //normalized directions for(int i=0; i((T)0,n); u[i][i]=1;} NRVec tstep(hp.my_initstep,n); //trial steps of the line minimizations, from the last step along each direction for(;; ++st.iter) { if(st.stop(f)) break; NRVec x0(x); T f0=f; T delta=0; //largest decrease along a single direction int ibig=0; for(int i=0; idelta) {delta=fprev-f; ibig=i;} } //new direction x-x0 replaces the one of the largest decrease, if Powell's test indicates it is worth NRVec unew = x-x0; T unorm=unew.norm(); if(unorm>0 && !e.exhausted()) { NRVec xe = x+unew; //extrapolated point T fe=e(xe); T a=f0-f-delta; T b=f0-fe; if(fe1) std::cout < dx = x-x0; st.step(f0-f,dx.norm()); } return st.finish(achieved); } //force instantiation template double linesearch_wolfe(optevaluator &e, NRVec &x, double &f, NRVec &g, const NRVec &p, const double alpha1, const linesearch_hyperparameters &lp); template class optstate; template class testproblem; template class BFGS_Optimizer; template class LBFGS_Optimizer; template class CG_Optimizer; template class NelderMead_Optimizer; template class Powell_Optimizer; template double linemin_brent(optevaluator &e, NRVec &x, double &f, const NRVec &u, const double t1, const double tol, const double abstol, const int maxiter, const int verbose); }//namespace