from array import * from math import sqrt def zerovector(n): a = array('d',range(n)) for i in range(n): a[i] = 0.0 return a def zeromatrix(n,m): a = range(n) for i in range(n): a[i] = zerovector(m) return a def copyvector(x): a = array('d',range(len(x))) for i in range(len(x)): a[i] = x[i] return a def copymatrix(x): n = len(x) m = len(x[0]) a = zeromatrix(n,m) for i in range(n): for j in range(m): a[i][j] = x[i][j] return a def transpose(x): n = len(x) m = len(x[0]) a = zeromatrix(m,n) for i in range(n): for j in range(m): a[j][i] = x[i][j] return a def dot(a,b): sum = 0.0 for i in range(len(a)): sum = sum + a[i]*b[i] return sum def mxm(a,b): n = len(a) k = len(a[0]) kk= len(b) m = len(b[0]) if kk != k: raise "matrices do not conform for multiplication" c = zeromatrix(n,m) for i in range(n): for j in range(m): sum = 0.0 for l in range(k): sum = sum + a[i][l]*b[l][j] c[i][j] = sum return c def mxv(a,b): n = len(a) k = len(a[0]) kk = len(b) if k != kk: raise "matrix and vector do not conform for multiplication" c = zerovector(n) for i in range(n): c[i] = dot(a[i],b) return c def printvector(a): n = len(a) for i in range(n): print ("%12.5e "%a[i]), print " " def printmatrix(a): n = len(a) for i in range(n): printvector(a[i]) def numderiv(func,x,step,eps): ''' Use central differences to compute the gradient and diagonal elements of the Hessian. func(x) = function to be differentiated x[] = (array) point at which to differentiate step[] = (array) remembers finite difference step between . successive calls. Set to zero on first call . or set close to appropriate value eps = expected precision in func Some care is taken to adjust the step so that the gradient and Hessian diagonal are estimated with about 4 digits of precision but some noise is unavaoidable due either to the noise in the function or cubic/higher terms in the Taylor expansion. ''' n = len(x) g = zerovector(n) h = zerovector(n) f0 = func(x) for i in range(n): if step[i] == 0.0: step[i] = max(abs(x[i])*0.01,0.0001) xi = x[i] while 1: x[i] = xi + step[i] f1 = func(x) if abs(f1-f0) < (1e4*eps): #print ' Increasing step ',i,step[i],abs(f1-f0) step[i] = step[i]*2.0 elif abs(f1-f0) > (1e5*eps): #print ' Decreasing step ',i,step[i],abs(f1-f0) step[i] = step[i]/3.0 else: break x[i] = xi - step[i] fm1 = func(x) x[i] = xi g[i] = (f1 - fm1)/(2*step[i]) h[i] = (f1 + fm1 - 2.0*f0)/(step[i]*step[i]) return (f0,g,h) def quadfit(alpha0, f0, alpha1, f1, alpha2, f2): ''' Given 3 points compute the gradient and hessian at point 0 using a quadratic fit. ''' delta1 = alpha1 - alpha0 delta2 = alpha2 - alpha0 d1 = (f1 - f0)/delta1 d2 = (f2 - f0)/delta2 h0 = 2.0*(d1 - d2)/(delta1-delta2) g0 = d1 - 0.5*h0*delta1 test1 = f0 + g0*delta1 + 0.5*h0*delta1*delta1 test2 = f0 + g0*delta2 + 0.5*h0*delta2*delta2 return (f0, g0, h0) def takestep(x0, s, alpha): x = zerovector(len(x0)) for j in range(len(x)): x[j] = x0[j] + s[j]*alpha return x def quadratic_step(trust, g0, h0): if h0 > 0: delta2 = -g0/h0 if abs(delta2) > trust: print " Step restriction: %f %f " % (delta2, trust) delta2 = abs(trust*delta2)/delta2 else: print " Negative curvature " delta2 = -abs(trust*g0)/g0 return delta2 def linesearch(func, x0, s, lsgrad, eps): # Assume here that some halfway OK preconditioning # is being used so we expect a step around unity. # Nevertheless, must exercise some caution. # First step in small increments until we've either # bracketed the minimum or gone downhil with enough # energy difference to start fitting print " Line search: step alpha grad hess value" print " ---- --------- -------- -------- ----------------" trust = 0.2 alpha0 = 0.0 f0 = func(x0) print " %9.2e %8.1e %16.8f" % \ (alpha0, lsgrad, f0) if lsgrad < 0: alpha1 = alpha0 + trust else: alpha1 = alpha0 - trust f1 = func(takestep(x0,s,alpha1)) print " %9.2e %16.8f" % \ (alpha1, f1) while f1 > f0: if trust < 0.00125: print " system is too badly conditioned for initial step" return (alpha0,f0) # Cannot seem to find my way trust = trust * 0.5 if lsgrad < 0: alpha1 = alpha0 + trust else: alpha1 = alpha0 - trust f1 = func(takestep(x0,s,alpha1)) print " %9.2e %16.8f" % \ (alpha1, f1) g0 = lsgrad h0 = (f1-f0-alpha1*g0)/alpha1**2 if f1 < f0: g0 = g0 + h0*(alpha1 - alpha0) alpha0, alpha1, f0, f1 = alpha1, alpha0, f1, f0 alpha2 = alpha0 + quadratic_step(trust,g0,h0) nbackstep =0 for iter in range(1,10): f2 = func(takestep(x0,s,alpha2)) #print ' alphas ', alpha0, alpha1, alpha2 #print ' fs ', f0, f1, f2 if iter == 1: f2prev = f2 print " %9.2e %16.8f" % \ (alpha2, f2) # Check for convergence or insufficient precision to proceed further if (abs(f0-f1)<(10*eps)) and (abs(f1-f2)<(10*eps)): print " ", print " Insufficient precision ... terminating LS" break if (f2-f2prev) > 0: # New point is higher than previous worst if nbackstep < 3: nbackstep = nbackstep + 1 print " ", print " Back stepping due to uphill step" trust = max(0.01,0.2*abs(alpha2 - alpha0)) # Reduce trust radius alpha2 = alpha0 + 0.2*(alpha2 - alpha0) continue elif (f2-f0) < 0: trust = min(4.0,trust*2.0) # Seem to have narrowed the search nbackstep = 0 f2prev = f2 # Order in increasing energy if f1 < f0: alpha0, alpha1, f0, f1 = alpha1, alpha0, f1, f0 if f2 < f0: alpha0, alpha2, f0, f2 = alpha2, alpha0, f2, f0 if f2 < f1: alpha1, alpha2, f1, f2 = alpha2, alpha1, f2, f1 (f0, g0, h0) = quadfit(alpha0, f0, alpha1, f1, alpha2, f2) print " %4i %9.2e %8.1e %8.1e %16.8f" % \ (iter, alpha0, g0, h0, f0) if (h0>0.0) and (abs(g0) < 0.03*abs(lsgrad)): print " ", print " gradient reduced 30-fold ... terminating LS" break # Determine the next step delta = quadratic_step(trust,g0,h0) alpha2 = alpha0 + delta df = g0*delta + 0.5*h0*delta*delta if abs(df) < 10.0*eps: print " ", print " projected energy reduction < 10*eps ... terminating LS" break return (alpha0, f0) def jacobi(ainput): ''' Diagonalize a real symmetric matrix using the variable threshold cyclic Jacobi method. (v,e) = jacobi(a) Input: a[n][m] is a real symmetric matrix Returns: (v,e) where v is the list of eigenvectors and e is an array of the corresponding eigenvalues in ascending order. v[k] is a vector containing the kth eigenvector. These satisfy A*Vt = Vt*e or V*A = e*V or sum(j)(a[i][j]v[k][j]) = e[k]*v[k][i] ''' a = copymatrix(ainput) n = len(a) m = len(a[0]) if n != m: raise 'Matrix must be square' for i in range(n): for j in range(m): if a[i][j] != a[j][i]: raise ' Matrix must be symmetric' tolmin = 1e-14 tol = 1e-4 v = zeromatrix(n,n) for i in range(n): v[i][i] = 1.0 maxd = 0.0 for i in range(n): maxd = max(abs(a[i][i]),maxd) for iter in range(50): nrot = 0 for i in range(n): for j in range(i+1,n): aii = a[i][i] ajj = a[j][j] daij = abs(a[i][j]) if daij > tol*maxd: # Screen small elements nrot = nrot + 1 s = aii - ajj ds = abs(s) if daij > (tolmin*ds): # Check for sufficient precision if (tol*daij) > ds: c = s = 1/sqrt(2.) else: t = a[i][j]/s u = 0.25/sqrt(0.25+t*t) c = sqrt(0.5+u) s = 2.*t*u/c for k in range(n): u = a[i][k] t = a[j][k] a[i][k] = s*t + c*u a[j][k] = c*t - s*u for k in range(n): u = a[k][i] t = a[k][j] a[k][i] = s*t + c*u a[k][j]= c*t - s*u for k in range(n): u = v[i][k] t = v[j][k] v[i][k] = s*t + c*u v[j][k] = c*t - s*u a[j][i] = a[i][j] = 0.0 maxd = max(maxd,abs(a[i][i]),abs(a[j][j])) if nrot == 0 and tol <= tolmin: break tol = max(tolmin,tol*0.99e-2) if nrot != 0: raise "Jacobi iteration did not converge in 50 passes" # Sort eigenvectors and values into increasing order e = zerovector(n) for i in range(n): e[i] = a[i][i] for j in range(i): if e[j] > e[i]: (e[i],e[j]) = (e[j],e[i]) (v[i],v[j]) = (v[j],v[i]) return (v,e) def hessian_update_bfgs(hp, dx, g, gp): ''' Apply the BFGS update to the approximate Hessian h[][]. hp[][] = Hessian matrix from previous iteration dx[] = Step from previous iteration . (dx[] = x[] - xp[] where xp[] is the previous point) g[] = gradient at current point gp[] = gradient at previous point Returns the updated hessian ''' n = len(hp) hdx = mxv(hp,dx) dg = zerovector(n) for i in range(n): dg[i] = g[i] - gp[i] dxhdx = dot(dx,hdx) dxdx = dot(dx,dx) dxdg = dot(dx,dg) dgdg = dot(dg,dg) h = copymatrix(hp) if (dxdx > 0.0) and (dgdg > 0.0) and (abs(dxdg/sqrt(dxdx*dgdg)) > 1.e-4): for i in range(n): for j in range(n): h[i][j] = h[i][j] + dg[i]*dg[j]/dxdg - hdx[i]*hdx[j]/dxhdx else: print ' BFGS not updating dxdg (%e), dgdg (%e), dxhdx (%f), dxdx(%e)' % (dxdg, dgdg, dxhdx, dxdx) return h def quasinr(func, guess, tol, eps, printvar=None): ''' Unconstrained minimization of a function of n variables without analytic derivatives using quasi-Newtwon with BFGS update and numerical gradients. func(x) is a function that takes an array of n values and returns the function value guess[] is an array of n values for the initial guess tol is the convergence criterion for the maximum value of the gradient eps is the expected precision in the function value printvar(x) is an optional user function to print the values of parameters each macro iteration ''' n = len(guess) x = copyvector(guess) s = zerovector(n) g = zerovector(n) gp = zerovector(n) step = zerovector(n) hessian = zeromatrix(n,n) alpha = 0.0 for iter in range(50*n): (value,g,h) = numderiv(func, x, step, eps) gmax = max(map(abs,g)) print ' ' print ' iter gmax value ' print ' ---- --------- ----------------' print "%4i %9.2e %16.8f" % (iter,gmax,value) if (printvar): printvar(x) if gmax < tol: print ' Converged!' break if iter == 0: for i in range(n): hessian[i][i] = max(abs(h[i]),1e-4) else: hessian = hessian_update_bfgs(hessian, s, g, gp) (v,e) = jacobi(hessian) emax = max(map(abs,e)) emin = emax*1e-4 # Control noise in small eigenvalues print '\n Eigenvalues of the Hessian:' printvector(e) # Transform to spectral form, take step, transform back gs = mxv(v,g) for i in range(n): if e[i] < emin: print ' Mode %d: small/negative eigenvalue (%f).' % (i, e[i]) s[i] = -gs[i]/emin else: s[i] = -gs[i]/e[i] s = mxv(transpose(v),s) # Apply overall step restriction ... better LS obviates this scale = 1.0 for i in range(n): trust = max(abs(x[i]),abs(x[i]/sqrt(max(1e-4,abs(hessian[i][i]))))) if abs(s[i]) > trust: print ' restricting ', i, trust, abs(x[i]), \ abs(x[i]/sqrt(abs(hessian[i][i]))), s[i] scale = min(scale,trust/abs(s[i])) if scale != 1.0: for i in range(n): s[i] = s[i]*scale (alpha,value) = linesearch(func, x, s, dot(s,g), eps) if alpha == 0.0: print ' Insufficient precision to proceed further' break for i in range(n): s[i] = s[i]*alpha x[i] = x[i] + s[i] gp[i]= g[i] return (value,x) def cgminold(func, dfunc, guess, tol): ''' Simple conjugate gradient assuming analtyic derivatives. ''' n = len(guess) x = copyvector(guess) s = zerovector(n) g = zerovector(n) gp= zerovector(n) value = func(x) for iter in range(10*n): g = dfunc(x) gmax = max(map(abs,g)) print ' ' print ' iter gmax value ' print ' ---- --------- ----------------' print "%4i %9.2e %16.8f" % (iter,gmax,value) if gmax < tol: print ' Converged!' break if (iter == 0) or ((iter%20) == 0): beta = 0.0 else: beta = (dot(g,g) - dot(g,gp))/(dot(s,g)-dot(s,gp)) for i in range(n): s[i] = -g[i] + beta*s[i] (alpha,value) = linesearch(func, x, s, dot(s,g), 1e-12) for i in range(n): s[i] = s[i]*alpha x[i] = x[i] + s[i] gp[i]= g[i] return (value,x) def cgmin(func, dfunc, guess, tol, precond=None, reset=None): ''' Conjugate gradient with optional preconditioning and use of analytic gradients. ''' n = len(guess) x = copyvector(guess) s = zerovector(n) g = zerovector(n) gp= zerovector(n) value = func(x) if not reset: reset = n reset = min(reset,n) for iter in range(10*n): g = dfunc(x) gmax = max(map(abs,g)) print ' ' print ' iter gmax value ' print ' ---- --------- ----------------' print "%4i %9.2e %16.8f" % (iter,gmax,value) if gmax < tol: print ' Converged!' break if precond: precondg = precond(g) else: precondg = g if (iter % reset) == 0: beta = 0.0 else: beta = (dot(precondg,g) - dot(precondg,gp))/(dot(s,g)-dot(s,gp)) for i in range(n): s[i] = -precondg[i] + beta*s[i] (alpha,value) = linesearch(func, x, s, dot(s,g), max(1e-16,abs(value)*1e-12)) for i in range(n): s[i] = s[i]*alpha x[i] = x[i] + s[i] gp[i]= g[i] return (value,x) def cgmin2(func, guess, tol, eps, printvar=None,reset=None): ''' Unconstrained minimization of a function of n variables without analytic derivatives using conjugate gradient with diagonal preconditioning. func(x) is a function that takes an array of n values and returns the function value guess[] is an array of n values for the initial guess tol is the convergence criterion for the maximum value of the gradient eps is the expected precision in the function value printvar(x) is an optional user function to print the values of parameters each iteration reset is the number of iterations between forced resets of the conjugacy. In principle this could be n but noise in the numerical gradients makes a smaller number a better choice. ''' n = len(guess) x = copyvector(guess) s = zerovector(n) g = zerovector(n) gp = zerovector(n) step = zerovector(n) precondg = zerovector(n) alpha = 0.0 if not reset: reset = n reset = min(reset,n) for iter in range(50*n): (value,g,hh) = numderiv(func, x, step, eps) gmax = max(map(abs,g)) print ' ' print ' iter gmax value ' print ' ---- --------- ----------------' print "%4i %9.2e %16.8f" % (iter,gmax,value) if (printvar): printvar(x) if gmax < tol: print ' Converged!' break if (iter % reset) == 0: # On the first iteration or if not applying conjugacy # we can recompute the diagonal preconditioner h = copyvector(hh) for i in range(n): h[i] = max(abs(h[i]),1e-6) # Preconditioning with the diagonal of the Hessian for i in range(n): precondg[i] = g[i] / h[i] # Should be able to reset every n steps but noisy gradients # means that we don't have enough info. if (iter % reset) == 0: if iter != 0: print" Resetting conjugacy" beta = 0.0 else: beta = (dot(precondg,g) - dot(precondg,gp))/(dot(s,g)-dot(s,gp)) for i in range(n): s[i] = -precondg[i] + beta*s[i] (alpha,value) = linesearch(func, x, s, dot(s,g), eps) if alpha == 0.0: # LS failed, probably due to lack of precision. if beta != 0.0: print "LS failed - trying preconditioned steepest descent direction" for i in range(n): s[i] = -g[i] (alpha,value) = linesearch(func, x, s, dot(s,g), eps) if alpha == 0.0: print " Insufficient precision to proceed further" break for i in range(n): s[i] = s[i]*alpha x[i] = x[i] + s[i] gp[i]= g[i] return (value,x) if __name__ == '__main__': def precond(g): # Used to test optional preconditioner for cgmin(). precondg = copyvector(g) for i in range(len(g)): precondg[i] = precondg[i]/(i+2.0) return precondg def df(x): d = zerovector(len(x)) for i in range(len(x)): d[i] = x[i]*(i+1) for j in range(len(x)): d[i] = d[i] + x[j] return d def f(x): sum = 0.0 for i in range(len(x)): for j in range(len(x)): sum = sum + 0.5*x[i]*x[j] for i in range(len(x)): sum = sum + 0.5*x[i]*x[i]*(i+1) return sum print '\n\n TESTING QUASI-NR SOLVER \n\n' quasinr(f, [1.,0.5,0.3,-0.4], 1e-4, 1e-10) print '\n\n TESTING GC WITH NUM. GRAD. AND DIAG. PRECOND.\n\n' cgmin2(f, [1.,0.5,0.3,-0.4], 1e-4, 1e-10, reset=20) print '\n\n TESTING GC WITH ANAL. GRAD. AND WITHOUT OPTIONAL PRECOND.\n\n' cgmin(f, df, [1.,0.5,0.3,-0.4], 1e-4) print '\n\n TESTING GC WITH ANAL. GRAD. AND WITH OPTIONAL PRECOND.\n\n' cgmin(f, df, [1.,0.5,0.3,-0.4], 1e-4, precond=precond) print '\n\n TESTING GC WITH ANAL. GRAD. AND NO PRECOND.\n\n' cgminold(f, df, [1.,0.5,0.3,-0.4], 1e-4) print '\n\n TESTING JACOBI EIGENSOLVER\n\n' n = 50 a = zeromatrix(n,n) for i in range(n): for j in range(i,n): a[j][i] = a[i][j] = (i*j+1.)/(i+j+1.) (v,e)= jacobi(a) print ' eigenvalues' printvector(e) #print ' v ' #printmatrix(v) ev = mxm(v,a) for i in range(n): err = 0.0 for j in range(n): err = max(err,abs(ev[i][j] - e[i]*v[i][j])) err = err/(n*max(1.0,abs(e[i]))) if err > 1e-12: print ' Error in eigenvector ', i, err