Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Newton-type Methods for Unconstrained Optimization

Reference: Section 2.4.2 in Biegler (2010)

import sys

if "google.colab" in sys.modules:
    !wget "https://raw.githubusercontent.com/ndcbe/optimization/main/notebooks/helper.py"
    # We do not need to install Pyomo or Ipopt, but we do want to format our plots
    # helper.easy_install()
    import helper
else:
    sys.path.insert(0, "../")
    import helper
helper.set_plotting_style()

# Load required Python libraries.
import matplotlib.pyplot as plt
import numpy as np
from scipy import linalg

Newton models for equations and optimization

A row-oriented Jacobian for equations

Let D⊆RnD\subseteq\mathbb R^n be open and F:D→Rn\mathbf F:D\to\mathbb R^n differentiable. Define (JF(x))ij=∂Fi(x)/∂xj(\mathbf J_F(\mathbf x))_{ij}=\partial F_i(\mathbf x)/\partial x_j for i,j∈{1,…,n}i,j\in\{1,\ldots,n\}. Linearizing at fixed xk\mathbf x^k gives

F(xk+p)=F(xk)+JF(xk)p+O(∥p∥2),\mathbf F(\mathbf x^k+\mathbf p)=\mathbf F(\mathbf x^k)+\mathbf J_F(\mathbf x^k)\mathbf p+O(\|\mathbf p\|^2),

with the quadratic remainder when the Jacobian is locally Lipschitz. Newton sets the linear model to zero and solves

JF(xk)pk=−F(xk),xk+1=xk+pk.\mathbf J_F(\mathbf x^k)\mathbf p^k=-\mathbf F(\mathbf x^k),\qquad \mathbf x^{k+1}=\mathbf x^k+\mathbf p^k.

The actual residual at the update need not be zero. The row-oriented Jacobian requires no extra transpose. An inverse is useful in analysis, but a numerical implementation solves the linear system.

Two Newton tangents and their zeros, with black axes and separate legend

A quadratic model for a scalar objective

For f∈C2(D)f\in C^2(D) on an open DD, write gk=∇f(xk)\mathbf g^k=\nabla f(\mathbf x^k) and Gk=∇2f(xk)\mathbf G^k=\nabla^2f(\mathbf x^k). With xk\mathbf x^k fixed,

mk(p)=f(xk)+(gk)Tp+12pTGkp.m_k(\mathbf p)=f(\mathbf x^k)+(\mathbf g^k)^T\mathbf p+\tfrac12\mathbf p^T\mathbf G^k\mathbf p.

Differentiating the model with respect to the chosen displacement gives

∇pmk=gk+Gkp,Gkpk=−gk.\nabla_{\mathbf p}m_k=\mathbf g^k+\mathbf G^k\mathbf p,\qquad \mathbf G^k\mathbf p^k=-\mathbf g^k.

This computes a model stationary point. It is the model minimum if Gk≻0\mathbf G^k\succ0. The model equals a quadratic objective exactly; for a nonlinear objective with locally Lipschitz Hessian its value remainder is O(∥p∥3)O(\|\mathbf p\|^3).

Sources: Biegler (2010), Eqs. (2.37)–(2.39), pp. 34–35; Nocedal and Wright (2006), §11.1, pp. 274–277. The gradient in the numerical examples below is a column vector.

A scalar quadratic model and its linear slope share the stationary displacementA nonlinear objective and its local quadratic model have different minimizers

The existing nonlinear-model sketch uses f(x)=ex−2xf(x)=e^x-2x at xk=1.8x^k=1.8. The Newton displacement is approximately -0.6694, giving xk+1=1.1306x^{k+1}=1.1306; the exact minimizer is the natural logarithm of 2. The objective drops from approximately 2.45 to 0.84, but the model stationary point still differs from the true minimizer.

A quadratic Newton step in Python

For f(x)=(x1−1)2+4(x2+2)2f(\mathbf x)=(x_1-1)^2+4(x_2+2)^2 at x0=[3,0]T\mathbf x^0=[3,0]^T, the gradient is [4,16]T[4,16]^T and the Hessian is diag⁡(2,8)\operatorname{diag}(2,8). Solve the Newton equation, then add the displacement to the current iterate. This cell runs on its own; no helper functions are needed.

import numpy as np
from scipy import linalg

x0 = np.array([3.0, 0.0])
g0 = np.array([4.0, 16.0])
H0 = np.diag([2.0, 8.0])
# Solve H0 @ p0 = -g0, then update the iterate.
p0 = linalg.solve(H0, -g0)
x1 = x0 + p0
print(p0, x1)
[-2. -2.] [ 1. -2.]

The step is [−2,−2]T[-2,-2]^T and the updated point is [1,−2]T[1,-2]^T. A quadratic objective equals its quadratic model everywhere, so one exact Newton step reaches its stationary point when the Hessian is nonsingular.

Checking a gradient norm in Python

Compute the gradient at the updated point using the same quadratic objective. A small gradient norm checks approximate stationarity; classification still requires curvature information. This cell also runs independently.

import numpy as np
from scipy import linalg

x1 = np.array([1.0, -2.0])
g1 = np.array([2 * (x1[0] - 1), 8 * (x1[1] + 2)])
# The default vector norm is the Euclidean norm.
gradient_norm = linalg.norm(g1)
print(gradient_norm)
0.0

A local model of the scalar website example

The scalar function from Descent and Globalization is

f(x)=12(x−1)4+(x+1)3−10x2+5x,f(x)=\tfrac12(x-1)^4+(x+1)^3-10x^2+5x,

with f′(x)=6−8x−3x2+2x3f'(x)=6-8x-3x^2+2x^3 and f′′(x)=−8−6x+6x2f''(x)=-8-6x+6x^2. At xk=−3x^k=-3, the values are f=15f=15, f′=−51f'=-51, and f′′=64f''=64. The quadratic model is mk(p)=15−51p+32p2m_k(p)=15-51p+32p^2, whose stationary displacement is pk=51/64p^k=51/64. The true objective at xk+1=−2.203125x^{k+1}=-2.203125 is approximately -8.66, and the true slope is approximately -12.32. The model slope is zero at this point; the objective slope is not.

The website's scalar function and its local model, with their corresponding slopes

The globalization notebook compares Newton, steepest descent, and modified curvature on this same function.

Curvature, descent, and step length

A strict local minimum need not have a positive-definite Hessian: f(x)=x4f(x)=x^4 at zero is a counterexample. The sufficient second-order conditions explicitly require a zero gradient and positive-definite Hessian. Continuity then preserves positive definiteness in a small neighborhood.

For g≠0\mathbf g\ne0, a direction is descent when gTp<0\mathbf g^T\mathbf p<0. If G≻0\mathbf G\succ0 and Gp=−g\mathbf G\mathbf p=-\mathbf g, then

gTp=−pTGp<0.\mathbf g^T\mathbf p=-\mathbf p^T\mathbf G\mathbf p<0.

This guarantees decrease for sufficiently small positive step lengths. It does not guarantee decrease for every α∈(0,1]\alpha\in(0,1]. For a C2C^2 objective, the exact Taylor identity is

f(x+αp)−f(x)=α∇f(x)Tp+12α2pT∇2f(x+tαp)p,t∈(0,1).f(\mathbf x+\alpha\mathbf p)-f(\mathbf x) =\alpha\nabla f(\mathbf x)^T\mathbf p +\tfrac12\alpha^2\mathbf p^T\nabla^2f(\mathbf x+t\alpha\mathbf p)\mathbf p, \quad t\in(0,1).

At fixed x,p\mathbf x,\mathbf p, the negative O(α)O(\alpha) term dominates the bounded O(α2)O(\alpha^2) remainder for small α\alpha. The intermediate point includes α\alpha.

A singular Hessian does not specify a unique basic Newton step. An indefinite Hessian can produce an ascent direction; an inaccurate positive-definite model can still overshoot. These are distinct reasons to modify curvature or control the accepted step length.

Sources: Biegler (2010), Eq. (2.40), p. 34, and §3.1, pp. 39–40; Nocedal and Wright (2006), pp. 22 and 46–47.

Full Newton step can overshoot despite negative initial slope

Algorithm 2.1: basic unit-step Newton

The following reference version adapts Biegler’s Algorithm 2.1 so each quantity is evaluated before it is tested.

  1. Choose x0\mathbf x^0, gradient tolerance ϵg\epsilon_g, and step tolerance ϵp\epsilon_p.

  2. Evaluate gk\mathbf g^k and Gk\mathbf G^k. A small gradient reports an approximately stationary candidate.

  3. If the Hessian is singular, report failure of the basic solve; otherwise solve Gkpk=−gk\mathbf G^k\mathbf p^k=-\mathbf g^k.

  4. A small step reports limited numerical movement, together with the current gradient norm.

  5. Otherwise take xk+1=xk+pk\mathbf x^{k+1}=\mathbf x^k+\mathbf p^k and repeat.

A candidate must still be classified; neither a small step nor an approximate Hessian proves a minimum. The diagnostic implementation below retains a post-step convergence test and also checks NaN derivatives, severe conditioning, and an iteration limit. Its experiments deliberately include failure cases.

Source: Biegler (2010), Algorithm 2.1, p. 35.

Helper Function

## Calculate gradient with central finite difference
def my_grad_approx(x, f, eps1, verbose=False):
    """
    Calculate gradient of function my_f using central difference formula

    Inputs:
        x - point for which to evaluate gradient
        f - function to consider
        eps1 - perturbation size

    Outputs:
        grad - gradient (vector)
    """

    n = len(x)
    grad = np.zeros(n)

    if verbose:
        print("***** my_grad_approx at x = ", x, "*****")

    for i in range(0, n):

        # Create vector of zeros except eps in position i
        e = np.zeros(n)
        e[i] = eps1

        # Finite difference formula
        my_f_plus = f(x + e)
        my_f_minus = f(x - e)

        # Diagnostics
        if verbose:
            print("e[", i, "] = ", e)
            print("f(x + e[", i, "]) = ", my_f_plus)
            print("f(x - e[", i, "]) = ", my_f_minus)

        grad[i] = (my_f_plus - my_f_minus) / (2 * eps1)

    if verbose:
        print("***** Done. ***** \n")

    return grad


## Calculate gradient using central finite difference and my_hes_approx
def my_hes_approx(x, grad, eps2):
    """
    Calculate Hessian of function my_f using central difference formula and my_grad

    Inputs:
        x - point for which to evaluate gradient
        grad - function to calculate the gradient
        eps2 - perturbation size (for Hessian NOT gradient approximation)

    Outputs:
        H - Hessian (matrix)
    """

    n = len(x)
    H = np.zeros([n, n])

    for i in range(0, n):
        # Create vector of zeros except eps in position i
        e = np.zeros(n)
        e[i] = eps2

        # Evaluate gradient twice
        grad_plus = grad(x + e)
        grad_minus = grad(x - e)

        # Notice we are building the Hessian by column (or row)
        H[:, i] = (grad_plus - grad_minus) / (2 * eps2)

    return H


def check_nan(A):
    return np.sum(np.isnan(A))


## Analyze Hessian.
def analyze_hes(B):
    print(B, "\n")

    if not check_nan(B):
        # Calculate eigenvalues
        l = linalg.eigvals(B)
        print("Eigenvalues: ", l, "\n")

Algorithm 1 Python Implementation

## Implement Alg 1 in a function
def alg1(x0, calc_f, calc_grad, calc_hes, eps1, eps2, verbose=False, max_iter=250):
    """
    Arguments:
        x0 - starting point
        calc_f - function that calculates f(x)
        calc_grad - function that calculates gradient(x)
        calc_hes - function that calculates hessian(x)

    Outputs:
        x - iteration history of x (decision variables)
        f - iteration history of f(x) (objective value)
        p - iteration history of p (steps)
    """

    # Allocate outputs as empty lists
    x = []
    f = []
    p = []

    # Store starting point
    x.append(x0)
    k = 0

    flag = True

    print("Iter. \tf(x) \t\t||grad(x)|| \t||p|| \t\tmin(lambda)")

    while flag:
        # Evaluate f(x) at current iteration
        f.append(calc_f(x[k]))

        # Evaluate gradient
        grad = calc_grad(x[k])

        if check_nan(grad):
            print("WARNING: gradient calculation returned NaN")
            break

        # Evaluate Hessian
        hes = calc_hes(x[k])

        if check_nan(hes):
            print("WARNING: Hessian calculation returned NaN")
            break

        if verbose:
            print("\n")
            print("k = ", k)
            print("x = ", x[k])
            print("grad = ", grad)
            print("hes = \n", hes)

        # Check if singular via condition number
        c = np.linalg.cond(hes)
        if c > 1e12:
            flag = False
            print("Warning: Hessian is near singular.")

        else:
            # Calculate step

            p.append(linalg.solve(hes, -grad))

            if verbose:
                print("p = ", p[k])

            # Take step. x[k+1] = x[k] + p[k]
            x.append(x[k] + p[k])

            # Calculate norms
            norm_p = linalg.norm(p[k])
            norm_g = linalg.norm(grad)

            # Calculate eigenvalues (for display only)
            ev = np.real(linalg.eigvals(hes))

            # print("k = ",k,"\t"f[k],"\t",norm_g,"\t",norm_p)
            print(
                k,
                "  \t{0: 1.4e} \t{1:1.4e} \t{2:1.4e} \t{3: 1.4e}".format(
                    f[k], norm_g, norm_p, np.min(ev)
                ),
            )

            # Check convergence criteria
            flag = (norm_p > eps1) and (norm_g > eps2)

            # Update iteration counter
            k = k + 1

        if k > max_iter:
            flag = False
            print("Maximum number of iterations.")
    print("Done.")
    print("x* = ", x[-1])

    return x, f, p

Quantified rates of convergence

The prefix Q means quotient: the ratios ek+1/eke_{k+1}/e_k and ek+1/ek2e_{k+1}/e_k^2 compare successive nonzero errors.

Let xk→x∗\mathbf x^k\to\mathbf x^* and ek=∥xk−x∗∥e_k=\|\mathbf x^k-\mathbf x^*\|. The convergence is at least Q-linear when

∃r∈(0,1), k0∈N:∀k≥k0,ek+1≤rek.\exists r\in(0,1),\ k_0\in\mathbb N:\quad\forall k\ge k_0,\quad e_{k+1}\le r e_k.

It is at least Q-quadratic when

∃K>0, k0∈N:∀k≥k0,ek+1≤Kek2.\exists K>0,\ k_0\in\mathbb N:\quad\forall k\ge k_0,\quad e_{k+1}\le K e_k^2.

These are eventual upper bounds; a sequence can satisfy a stronger rate too. For ek=10−3e_k=10^{-3}, r=0.5r=0.5 gives ek+1≤5×10−4e_{k+1}\le5\times10^{-4}, while K=1K=1 gives ek+1≤10−6e_{k+1}\le10^{-6}. Strict minimality by itself supplies neither the rate nor the theorem’s nondegeneracy: unit-step Newton on f(x)=x4f(x)=x^4 has xk+1=2xk/3x^{k+1}=2x^k/3 for xk≠0x^k\ne0, so its exact error ratio is 2/32/3, and f′′(0)=0f''(0)=0.

Source: Nocedal and Wright (2006), Appendix A.2, Eq. (A.34), p. 619. The following theorem and proofs specify the neighborhood and constants required for Newton’s quadratic bounds.

Quadratic Convergence Proof

Theorem 2.20 (Biegler, 2010)

Assume f∈C2(D)f\in C^2(D) on an open neighborhood DD of x∗\mathbf x^*, with Lipschitz Hessian there, ∇f(x∗)=0\nabla f(\mathbf x^*)=\mathbf0, and ∇2f(x∗)≻0\nabla^2f(\mathbf x^*)\succ0. Then, by applying Algorithm 2.1 and with a sufficiently close x0\mathbf{x}_0, there exists a constant L^>0\hat{L} > 0 such that:

  1. The convergence rate for {xk}\{\mathbf{x}^k\} is quadratic, i.e., ∥xk+1−x∗∥≤L^∥xk−x∗∥2\| \mathbf{x}^{k+1} - \mathbf{x}^* \| \leq \hat{L} \| \mathbf{x}^k - \mathbf{x}^* \|^2

  2. The convergence rate for {∇f(xk)}\{\nabla f(\mathbf{x}^k)\} is also quadratic

Iterate-error proof and the integral-norm tools

The goal is to show that unit-step Newton is well defined near x∗\mathbf x^*, that its iterates remain in that neighborhood and converge, and that the resulting error bound is quadratic. Use the local convergence theorem: f∈C2(D)f\in C^2(D) on an open neighborhood of x∗\mathbf x^*, ∇f(x∗)=0\nabla f(\mathbf x^*)=\mathbf0, the Hessian is Lipschitz there, and ∇2f(x∗)≻0\nabla^2f(\mathbf x^*)\succ0.

Use the Euclidean vector norm and its induced matrix norm. The tools are

∥Av∥≤∥A∥∥v∥,∥AB∥≤∥A∥∥B∥,∥∫01w(t) dt∥≤∫01∥w(t)∥ dt.\|\mathbf A\mathbf v\|\le\|\mathbf A\|\|\mathbf v\|,\qquad \|\mathbf A\mathbf B\|\le\|\mathbf A\|\|\mathbf B\|,\qquad \left\|\int_0^1\mathbf w(t)\,dt\right\|\le\int_0^1\|\mathbf w(t)\|\,dt.

The last bound is the triangle inequality for a vector integral. Together these give

∥A∫01M(t)v dt∥≤∥A∥∫01∥M(t)∥∥v∥ dt.\left\|\mathbf A\int_0^1\mathbf M(t)\mathbf v\,dt\right\| \le\|\mathbf A\|\int_0^1\|\mathbf M(t)\|\|\mathbf v\|\,dt.

1. Fix a neighborhood and its constants. Since f∈C2f\in C^2, the symmetric Hessian is continuous. Positive definiteness at x∗\mathbf x^* gives λmin⁡(∇2f(x∗))>0\lambda_{\min}(\nabla^2f(\mathbf x^*))>0. By continuity of the eigenvalues, choose r>0r>0 with B=B(x∗,r)={x:∥x−x∗∥<r}⊂DB=B(\mathbf x^*,r)=\{\mathbf x:\|\mathbf x-\mathbf x^*\|<r\}\subset D and m>0m>0 such that λmin⁡(∇2f(x))≥m\lambda_{\min}(\nabla^2f(\mathbf x))\ge m for every x∈B\mathbf x\in B. Hence every Hessian in BB is invertible and

∥∇2f(x)−1∥≤1/m=:C(x∈B).\|\nabla^2f(\mathbf x)^{-1}\|\le 1/m=:C\quad(\mathbf x\in B).

For a symmetric positive definite matrix, its inverse has Euclidean induced norm 1/λmin⁡1/\lambda_{\min}. The Hessian Lipschitz assumption supplies a fixed constant L>0L>0 with

∥∇2f(u)−∇2f(v)∥≤L∥u−v∥(u,v∈B).\|\nabla^2f(\mathbf u)-\nabla^2f(\mathbf v)\|\le L\|\mathbf u-\mathbf v\|\quad(\mathbf u,\mathbf v\in B).

If the Hessian is constant, any positive LL is a valid upper-bound constant. Fix this ball and these constants before choosing the smaller initial radius in Step 6.

2. Derive the exact gradient identity. Write ek=xk−x∗\mathbf e_k=\mathbf x^k-\mathbf x^*, gk=∇f(xk)\mathbf g^k=\nabla f(\mathbf x^k), and Gk=∇2f(xk)\mathbf G^k=\nabla^2f(\mathbf x^k). Suppose xk∈B\mathbf x^k\in B and define

z(t)=x∗+tek=(1−t)x∗+txk,0≤t≤1.\mathbf z(t)=\mathbf x^*+t\mathbf e_k=(1-t)\mathbf x^*+t\mathbf x^k,\qquad 0\le t\le1.

A ball is convex, so this entire segment stays in BB. For ψ(t)=∇f(z(t))\boldsymbol\psi(t)=\nabla f(\mathbf z(t)), the chain rule gives ψ′(t)=∇2f(z(t))ek\boldsymbol\psi'(t)=\nabla^2f(\mathbf z(t))\mathbf e_k. Apply the one-variable fundamental theorem of calculus (FTC) to each gradient component:

gk−∇f(x∗)=ψ(1)−ψ(0)=∫01∇2f(z(t))ek dt.\mathbf g^k-\nabla f(\mathbf x^*)=\boldsymbol\psi(1)-\boldsymbol\psi(0) =\int_0^1\nabla^2f(\mathbf z(t))\mathbf e_k\,dt.

Stationarity makes ∇f(x∗)=0\nabla f(\mathbf x^*)=\mathbf0. More generally, the same chain-rule/FTC argument gives

∇f(x+p)−∇f(x)=∫01∇2f(x+tp)p dt.\nabla f(\mathbf x+\mathbf p)-\nabla f(\mathbf x) =\int_0^1\nabla^2f(\mathbf x+t\mathbf p)\mathbf p\,dt.

In one dimension, u=x+tpu=x+tp, du=p dtdu=p\,dt turns the integral into ∫xx+pf′′(u) du=f′(x+p)−f′(x)\int_x^{x+p}f''(u)\,du=f'(x+p)-f'(x); its oriented limits include negative pp.

3. Rewrite the next iterate error as a Hessian difference. Unit-step Newton solves Gkpk=−gk\mathbf G^k\mathbf p^k=-\mathbf g^k and updates xk+1=xk+pk\mathbf x^{k+1}=\mathbf x^k+\mathbf p^k. The inverse exists by Step 1. Thus

ek+1=ek−(Gk)−1gk=(Gk)−1[Gkek−gk]=(Gk)−1∫01[Gk−∇2f(z(t))]ek dt.\begin{aligned} \mathbf e_{k+1} &=\mathbf e_k-(\mathbf G^k)^{-1}\mathbf g^k\\ &=(\mathbf G^k)^{-1}[\mathbf G^k\mathbf e_k-\mathbf g^k]\\ &=(\mathbf G^k)^{-1}\int_0^1[\mathbf G^k-\nabla^2f(\mathbf z(t))]\mathbf e_k\,dt. \end{aligned}

The second line factors the inverse using ek=(Gk)−1Gkek\mathbf e_k=(\mathbf G^k)^{-1}\mathbf G^k\mathbf e_k. For the last line, write the constant Gkek\mathbf G^k\mathbf e_k as its integral over [0,1][0,1], then substitute Step 2. This is an exact identity before any inequality is applied.

An actual local ball and the enlarged iterate-error segment with its distance bound

4. Use the remaining segment length in the Lipschitz bound. Subtracting the intermediate point gives

xk−z(t)=(1−t)ek,∥Gk−∇2f(z(t))∥≤L∥xk−z(t)∥=L(1−t)∥ek∥.\mathbf x^k-\mathbf z(t)=(1-t)\mathbf e_k,\qquad \|\mathbf G^k-\nabla^2f(\mathbf z(t))\| \le L\|\mathbf x^k-\mathbf z(t)\|=L(1-t)\|\mathbf e_k\|.

Both Hessian arguments are in BB by Step 2, which is why the local Lipschitz bound applies.

5. Take norms and integrate. Apply the induced-norm bound and the integral triangle inequality to Step 3, then use Steps 1 and 4:

∥ek+1∥≤∥(Gk)−1∥∫01∥Gk−∇2f(z(t))∥∥ek∥ dt≤CL∥ek∥2∫01(1−t) dt=12CL∥ek∥2.\begin{aligned} \|\mathbf e_{k+1}\| &\le\|(\mathbf G^k)^{-1}\|\int_0^1\|\mathbf G^k-\nabla^2f(\mathbf z(t))\|\|\mathbf e_k\|\,dt\\ &\le CL\|\mathbf e_k\|^2\int_0^1(1-t)\,dt =\tfrac12CL\|\mathbf e_k\|^2. \end{aligned}

The first inequality uses induced norms and the integral triangle inequality, not a scalar Cauchy-Schwarz argument. The factor 1/21/2 comes from integrating the segment distance. Set K=CL/2K=CL/2; using the looser constant CLCL is also valid.

6. Close the local induction and prove convergence. Keep B,C,LB,C,L fixed. Choose 0<δ<r0<\delta<r such that q=Kδ<1q=K\delta<1, and initialize ∥e0∥<δ\|\mathbf e_0\|<\delta. If ∥ek∥<δ\|\mathbf e_k\|<\delta, Step 5 gives

∥ek+1∥≤K∥ek∥2≤q∥ek∥<δ.\|\mathbf e_{k+1}\|\le K\|\mathbf e_k\|^2 \le q\|\mathbf e_k\|<\delta.

Induction therefore keeps every iterate in B(x∗,δ)⊂BB(\mathbf x^*,\delta)\subset B, so all the solves and segment estimates used above remain valid. Repeated contraction gives ∥ek∥≤qk∥e0∥→0\|\mathbf e_k\|\le q^k\|\mathbf e_0\|\to0. The stronger inequality ∥ek+1∥≤K∥ek∥2\|\mathbf e_{k+1}\|\le K\|\mathbf e_k\|^2 proves at least Q-quadratic convergence. If an iterate equals x∗\mathbf x^*, Newton has already reached the solution and the same bound holds with zero error.

The gradient-residual proof uses the same FTC, cancellation, and Lipschitz-remainder mechanics along a different segment.

Sources: Biegler (2010), Theorem 2.14, pp. 27-28, and Theorem 2.20, p. 35, with the corrected segment in the errata. The segment here runs from x∗\mathbf x^* to xk\mathbf x^k, the reverse parametrization of the corrected book segment.

Gradient-residual proof

Under the same hypotheses and sufficiently close initialization as the local convergence theorem, the gradient residual obeys

∥gk+1∥≤12LC2∥gk∥2,\|\mathbf g^{k+1}\|\le\tfrac12LC^2\|\mathbf g^k\|^2,

where gk=∇f(xk)\mathbf g^k=\nabla f(\mathbf x^k), Gk=∇2f(xk)\mathbf G^k=\nabla^2f(\mathbf x^k), LL is the Hessian Lipschitz constant, and CC bounds the inverse Hessian on the fixed local ball. This is the second result stated in the handout. The full derivation follows the same mechanics as the iterate-error proof: choose a segment, use the chain rule and FTC, cancel the linear part, and bound the remaining Hessian difference.

1. Check that the step segment stays in the neighborhood. The iterate-error induction keeps both xk\mathbf x^k and xk+1\mathbf x^{k+1} in B(x∗,δ)⊂BB(\mathbf x^*,\delta)\subset B. Since a ball is convex,

y(t)=xk+tpk=(1−t)xk+txk+1∈B,0≤t≤1.\mathbf y(t)=\mathbf x^k+t\mathbf p^k=(1-t)\mathbf x^k+t\mathbf x^{k+1}\in B,\qquad 0\le t\le1.

The Hessian Lipschitz bound therefore applies on the entire step, and ∥(Gk)−1∥≤C\|(\mathbf G^k)^{-1}\|\le C at its starting point.

Both Newton endpoints and their segment stay in the local ball

2. Apply FTC and cancel the Newton model. With ϕ(t)=∇f(y(t))\boldsymbol\phi(t)=\nabla f(\mathbf y(t)), the chain rule gives ϕ′(t)=∇2f(y(t))pk\boldsymbol\phi'(t)=\nabla^2f(\mathbf y(t))\mathbf p^k. Componentwise FTC gives

gk+1−gk=∫01∇2f(y(t))pk dt.\mathbf g^{k+1}-\mathbf g^k=\int_0^1\nabla^2f(\mathbf y(t))\mathbf p^k\,dt.

The Newton equation is gk+Gkpk=0\mathbf g^k+\mathbf G^k\mathbf p^k=\mathbf0. Subtract its zero linear-model residual and integrate the constant matrix term over [0,1][0,1]:

gk+1=gk+1−gk−Gkpk=∫01[∇2f(y(t))−Gk]pk dt.\begin{aligned} \mathbf g^{k+1} &=\mathbf g^{k+1}-\mathbf g^k-\mathbf G^k\mathbf p^k\\ &=\int_0^1[\nabla^2f(\mathbf y(t))-\mathbf G^k]\mathbf p^k\,dt. \end{aligned}

This identity isolates the exact gradient-model remainder; there is no unestimated Taylor remainder.

3. Bound the Hessian difference by the step distance. The intermediate point is t∥pk∥t\|\mathbf p^k\| from xk\mathbf x^k, so

∥∇2f(y(t))−Gk∥≤L∥y(t)−xk∥=Lt∥pk∥.\|\nabla^2f(\mathbf y(t))-\mathbf G^k\| \le L\|\mathbf y(t)-\mathbf x^k\|=Lt\|\mathbf p^k\|.

Applying the integral triangle inequality and the induced-norm bound to Step 2 yields

∥gk+1∥≤∫01∥∇2f(y(t))−Gk∥∥pk∥ dt≤L∥pk∥2∫01t dt=12L∥pk∥2.\begin{aligned} \|\mathbf g^{k+1}\| &\le\int_0^1\|\nabla^2f(\mathbf y(t))-\mathbf G^k\|\|\mathbf p^k\|\,dt\\ &\le L\|\mathbf p^k\|^2\int_0^1t\,dt =\tfrac12L\|\mathbf p^k\|^2. \end{aligned}

Here the integral of tt supplies 1/21/2, whereas the iterate-error proof integrates 1−t1-t.

4. Express the step bound in terms of the current gradient. The Newton solve and the uniform inverse bound give

∥pk∥=∥(Gk)−1gk∥≤∥(Gk)−1∥∥gk∥≤C∥gk∥.\|\mathbf p^k\|=\|(\mathbf G^k)^{-1}\mathbf g^k\| \le\|(\mathbf G^k)^{-1}\|\|\mathbf g^k\|\le C\|\mathbf g^k\|.

Substitution into Step 3 proves

∥gk+1∥≤12LC2∥gk∥2.\|\mathbf g^{k+1}\|\le\tfrac12LC^2\|\mathbf g^k\|^2.

The iterate-error result already establishes xk→x∗\mathbf x^k\to\mathbf x^*; continuity of the gradient and stationarity imply gk→0\mathbf g^k\to\mathbf0. Together with this squared-residual bound, that gives at least Q-quadratic convergence of the gradient norm. Small steps alone do not imply the rate: the uniform inverse bound and Hessian Lipschitz assumption do the work. The local positive-definiteness hypothesis is sufficient here; for the analogous root-finding theorem a nonsingular limiting Jacobian suffices and does not by itself classify a stationary point as a minimum.

Source: Biegler (2010), Theorem 2.20 and Eq. (2.43), p. 35. The handout states this result and links here for the full second derivation.

Test Problem

Example 2.19 Consider the following two-variable unconstrained optimization problem:

min⁡f(x)=αexp⁡(−β)u=x1−0.8,v=x2−(a1+a2u2(1−u)12−a3u),α=−b1+b2u2(1+u)12+b3u,β=c1v2(1−c2v)/(1+c3u2)\begin{align} \min f(\mathbf{x}) &= \alpha \exp(-\beta) \\ u &= x_1 - 0.8, \\ v &= x_2 - (a_1 + a_2u^2(1-u)^{\frac{1}{2}} - a_3u), \\ \alpha &= -b_1 + b_2u^2(1+u)^{\frac{1}{2}} + b_3u, \\ \beta &= c_1 v^2(1 - c_2v) / (1 + c_3u^2) \end{align}

with

aT=[0.3,0.6,0.2],bT=[5,26,3],cT=[40,1,10].\begin{align} \mathbf{a}^T &= [0.3, 0.6, 0.2], \nonumber \\ \mathbf{b}^T &= [5, 26, 3], \nonumber \\ \mathbf{c}^T &= [40, 1, 10]. \nonumber \end{align}

The book reports the rounded candidate x∗=[0.7395,0.3144]\mathbf{x}^* = [0.7395, 0.3144] with f(x∗)=−5.0893f(\mathbf{x}^*) = -5.0893.

The nearby exact stationary point has zero gradient; rounding its coordinates does not preserve exact stationarity. The book reports the following approximate Hessian there:

∇2f(x∗)=[77.012108.334108.334392.767],\begin{align} \nabla^2 f(\mathbf{x}^*) &= \begin{bmatrix} 77.012 & 108.334 \\ 108.334 & 392.767 \end{bmatrix}, \end{align}

which has eigenvalues λ=43.417\lambda = 43.417 and λ=426.362\lambda = 426.362.

Source: Biegler (2010), Example 2.19, p. 30. The canonical optimality notebook compares rounded and refined points and reports actual derivative residuals; the Newton experiments below use the same objective for a distinct iteration task.

## Define Python function to calculate objective
def my_f(x, verbose=False):
    """Evaluate function given above at point x

    Inputs:
        x - vector with 2 elements

    Outputs:
        f - function value (scalar)
    """
    # Constants
    a = np.array([0.3, 0.6, 0.2])
    b = np.array([5, 26, 3])
    c = np.array([40, 1, 10])

    # Intermediates. Recall Python indices start at 0
    u = x[0] - 0.8
    s = np.sqrt(1 - u)
    s2 = np.sqrt(1 + u)
    v = x[1] - (a[0] + a[1] * u**2 * s - a[2] * u)
    alpha = (
        -b[0] + b[1] * u**2 * s2 + b[2] * u
    )  # September 5, 2018: changed 's' to 's2'
    beta = c[0] * v**2 * (1 - c[1] * v) / (1 + c[2] * u**2)
    f = alpha * np.exp(-beta)

    if verbose:
        print("##### my_f at x = ", x, "#####")
        print("u = ", u)
        print("sqrt(1-u) = ", s)
        print("sqrt(1+u) = ", s2)
        print("v = ", v)
        print("alpha = ", alpha)
        print("beta = ", beta)
        print("f(x) = ", f)
        print("##### Done. #####\n")

    return f

Checking Hessian eigenvalues in Python

Use the reported symmetric Hessian from Example 2.19. scipy.linalg.eigvalsh returns its real eigenvalues. Both are positive, so this matrix is positive definite. The local-minimum conclusion also requires a zero gradient at the point under consideration. The displayed matrix and coordinates are rounded data, not an exact stationarity certificate.

import numpy as np
from scipy import linalg

# Biegler Example 2.19: the reported approximate Hessian.
H = np.array([[77.012, 108.334], [108.334, 392.767]])
# eigvalsh uses symmetry and returns real eigenvalues.
eigenvalues = linalg.eigvalsh(H)
print(eigenvalues)
[ 43.41741179 426.36158821]

Two starts: a minimum and a saddle

Biegler’s Example 2.21 applies unit-step Newton to the Example 2.19 objective defined above. Starting from [0.8,0.3]T[0.8,0.3]^T gives the minimum near [0.7395,0.31435]T[0.7395,0.31435]^T; starting from [1.0,0.5]T[1.0,0.5]^T gives a saddle near [1.0768,0.9504]T[1.0768,0.9504]^T. Both endpoint gradient norms are below 10-12. The saddle Hessian has one positive and one negative eigenvalue, approximately 11.752 and -3.034.

Newton paths from the two starts, with gradient-norm histories

The prepared figure uses the analytic gradient and a complex-step Hessian. The complete plotting implementation contains the objective, both derivatives, the Newton loop, and the checks against Biegler’s Tables 2.2 and 2.3. The finite histories illustrate fast convergence; they do not prove an asymptotic rate. Newton applied to ∇f=0\nabla f=\mathbf0 can converge quadratically to a saddle with a nonsingular Hessian when the Hessian is locally Lipschitz and the initial point is sufficiently close (Nocedal and Wright, Theorem 11.2, p. 276).

The following experiments use the same objective with finite-difference derivatives and additional starting points. They retain their deliberate failure cases.

Starting Point Near Optimal Solution

## Test on example

# Specify convergence criteria
eps1 = 1e-8
eps2 = 1e-4

# Create a Lambda (anonymous) function for gradient calculation
calc_grad = lambda x: my_grad_approx(x, my_f, 1e-6)

# Create a Lambda (anonymous) function for Hessian calculation
calc_hes = lambda x: my_hes_approx(x, calc_grad, 1e-6)

# Specify starting point
x0 = np.array([0.7, 0.3])

# Call optimization routine
x, f, p = alg1(x0, my_f, calc_grad, calc_hes, eps1, eps2)

# Actual Hessian
print("Hessian at x*= ")
analyze_hes(my_hes_approx(x[-1], calc_grad, 1e-6))
Iter. 	f(x) 		||grad(x)|| 	||p|| 		min(lambda)
0   	-4.9246e+00 	1.0874e+01 	4.1946e-02 	 4.9368e+01
1   	-5.0888e+00 	6.2736e-01 	1.9752e-03 	 4.2541e+01
2   	-5.0893e+00 	2.4210e-03 	3.0032e-05 	 4.3424e+01
3   	-5.0893e+00 	2.9199e-07 	3.1620e-09 	 4.3418e+01
Done.
x* =  [0.73950546 0.3143601 ]
Hessian at x*= 
[[ 77.01173033 108.33423048]
 [108.33423048 392.76693009]] 

Eigenvalues:  [ 43.41702924+0.j 426.36163117+0.j] 

Activity: A Different Starting Point

Try with x0=[0,0]T\mathbf{x}_0 = [0,0]^T

# Specify starting point
x0 = np.array([0, 0])

# Call optimization routine
x, f, p = alg1(x0, my_f, calc_grad, calc_hes, eps1, eps2)

# Actual Hessian
print("Hessian at x* = ")
analyze_hes(my_hes_approx(x[-1], calc_grad, 1e-6))
Iter. 	f(x) 		||grad(x)|| 	||p|| 		min(lambda)
0   	 1.6212e-06 	1.5568e-04 	3.8590e-02 	-2.9496e-03
1   	 5.4102e-07 	5.5405e-05 	3.6202e-02 	-1.0900e-03
Done.
x* =  [-0.00124597 -0.07478209]
Hessian at x* = 
[[0.0001531  0.00055527]
 [0.00055527 0.00014766]] 

Eigenvalues:  [ 0.00070565+0.j -0.00040489+0.j] 

Activity: Let’s break it

Activity: Try x0=[−0.2,−0.2]T\mathbf{x}_0 = [-0.2, -0.2]^T. Why does the gradient or Hessian return NaN? Hint: redefine calc_grad and create my_f_verbose using verbose=True.

# Specify starting point
x0 = np.array([-0.2, -0.2])

# Create a Lambda (anonymous) function for f(x) calculation with verbose output
my_f_verbose = lambda x: my_f(x, verbose=True)

# Create a Lambda (anonymous) function for gradient calculation
calc_grad = lambda x: my_grad_approx(x, my_f_verbose, 1e-6, verbose=True)

# Call optimization routine
x, f, p = alg1(x0, my_f, calc_grad, calc_hes, eps1, eps2, verbose=True)

# Actual Hessian
print("Hessian at x* = ")
analyze_hes(my_hes_approx(x[-1], calc_grad, 1e-6))
Iter. 	f(x) 		||grad(x)|| 	||p|| 		min(lambda)
***** my_grad_approx at x =  [-0.2 -0.2] *****
##### my_f at x =  [-0.199999 -0.2     ] #####
u =  -0.9999990000000001
sqrt(1-u) =  1.4142132088196604
sqrt(1+u) =  0.0009999999999588667
v =  -1.5485260282367943
alpha =  -7.973997052001044
beta =  22.222565160998283
f(x) =  -1.780486346234917e-09
##### Done. #####

##### my_f at x =  [-0.200001 -0.2     ] #####
u =  -1.0000010000000001
sqrt(1-u) =  1.4142139159264415
sqrt(1+u) =  nan
v =  -1.5485302466134128
alpha =  nan
beta =  22.222642208927322
f(x) =  nan
##### Done. #####

e[ 0 ] =  [1.e-06 0.e+00]
f(x + e[ 0 ]) =  -1.780486346234917e-09
f(x - e[ 0 ]) =  nan
##### my_f at x =  [-0.2      -0.199999] #####
u =  -1.0
sqrt(1-u) =  1.4142135623730951
sqrt(1+u) =  0.0
v =  -1.548527137423857
alpha =  -8.0
beta =  22.222566263573963
f(x) =  -1.786290485440547e-09
##### Done. #####

##### my_f at x =  [-0.2      -0.200001] #####
u =  -1.0
sqrt(1-u) =  1.4142135623730951
sqrt(1+u) =  0.0
v =  -1.548529137423857
alpha =  -8.0
beta =  22.22264110629725
f(x) =  -1.7861567995988046e-09
##### Done. #####

e[ 1 ] =  [0.e+00 1.e-06]
f(x + e[ 1 ]) =  -1.786290485440547e-09
f(x - e[ 1 ]) =  -1.7861567995988046e-09
***** Done. ***** 

WARNING: gradient calculation returned NaN
Done.
x* =  [-0.2 -0.2]
Hessian at x* = 
***** my_grad_approx at x =  [-0.199999 -0.2     ] *****
##### my_f at x =  [-0.199998 -0.2     ] #####
u =  -0.999998
sqrt(1-u) =  1.414212855266137
sqrt(1+u) =  0.0014142135623541761
v =  -1.5485239190522238
alpha =  -7.963224594456855
beta =  22.22252663717692
f(x) =  -1.778149501075135e-09
##### Done. #####

##### my_f at x =  [-0.2 -0.2] #####
u =  -1.0
sqrt(1-u) =  1.4142135623730951
sqrt(1+u) =  0.0
v =  -1.5485281374238569
alpha =  -8.0
beta =  22.22260368491507
f(x) =  -1.7862236413056787e-09
##### Done. #####

e[ 0 ] =  [1.e-06 0.e+00]
f(x + e[ 0 ]) =  -1.778149501075135e-09
f(x - e[ 0 ]) =  -1.7862236413056787e-09
##### my_f at x =  [-0.199999 -0.199999] #####
u =  -0.9999990000000001
sqrt(1-u) =  1.4142132088196604
sqrt(1+u) =  0.0009999999999588667
v =  -1.5485250282367944
alpha =  -7.973997052001044
beta =  22.222527739675733
f(x) =  -1.7805529756354463e-09
##### Done. #####

##### my_f at x =  [-0.199999 -0.200001] #####
u =  -0.9999990000000001
sqrt(1-u) =  1.4142132088196604
sqrt(1+u) =  0.0009999999999588667
v =  -1.5485270282367944
alpha =  -7.973997052001044
beta =  22.222602582361898
f(x) =  -1.7804197192545872e-09
##### Done. #####

e[ 1 ] =  [0.e+00 1.e-06]
f(x + e[ 1 ]) =  -1.7805529756354463e-09
f(x - e[ 1 ]) =  -1.7804197192545872e-09
***** Done. ***** 

***** my_grad_approx at x =  [-0.200001 -0.2     ] *****
##### my_f at x =  [-0.2 -0.2] #####
u =  -1.0
sqrt(1-u) =  1.4142135623730951
sqrt(1+u) =  0.0
v =  -1.5485281374238569
alpha =  -8.0
beta =  22.22260368491507
f(x) =  -1.7862236413056787e-09
##### Done. #####

##### my_f at x =  [-0.200002 -0.2     ] #####
u =  -1.000002
sqrt(1-u) =  1.4142142694796995
sqrt(1+u) =  nan
v =  -1.5485323558054604
alpha =  nan
beta =  22.222680733035
f(x) =  nan
##### Done. #####

e[ 0 ] =  [1.e-06 0.e+00]
f(x + e[ 0 ]) =  -1.7862236413056787e-09
f(x - e[ 0 ]) =  nan
##### my_f at x =  [-0.200001 -0.199999] #####
u =  -1.0000010000000001
sqrt(1-u) =  1.4142139159264415
sqrt(1+u) =  nan
v =  -1.5485292466134128
alpha =  nan
beta =  22.22260478756765
f(x) =  nan
##### Done. #####

##### my_f at x =  [-0.200001 -0.200001] #####
u =  -1.0000010000000001
sqrt(1-u) =  1.4142139159264415
sqrt(1+u) =  nan
v =  -1.548531246613413
alpha =  nan
beta =  22.222679630328063
f(x) =  nan
##### Done. #####

e[ 1 ] =  [0.e+00 1.e-06]
f(x + e[ 1 ]) =  nan
f(x - e[ 1 ]) =  nan
***** Done. ***** 

***** my_grad_approx at x =  [-0.2      -0.199999] *****
##### my_f at x =  [-0.199999 -0.199999] #####
u =  -0.9999990000000001
sqrt(1-u) =  1.4142132088196604
sqrt(1+u) =  0.0009999999999588667
v =  -1.5485250282367944
alpha =  -7.973997052001044
beta =  22.222527739675733
f(x) =  -1.7805529756354463e-09
##### Done. #####

##### my_f at x =  [-0.200001 -0.199999] #####
u =  -1.0000010000000001
sqrt(1-u) =  1.4142139159264415
sqrt(1+u) =  nan
v =  -1.5485292466134128
alpha =  nan
beta =  22.22260478756765
f(x) =  nan
##### Done. #####

e[ 0 ] =  [1.e-06 0.e+00]
f(x + e[ 0 ]) =  -1.7805529756354463e-09
f(x - e[ 0 ]) =  nan
##### my_f at x =  [-0.2      -0.199998] #####
u =  -1.0
sqrt(1-u) =  1.4142135623730951
sqrt(1+u) =  0.0
v =  -1.548526137423857
alpha =  -8.0
beta =  22.222528842273906
f(x) =  -1.7863573320035263e-09
##### Done. #####

##### my_f at x =  [-0.2 -0.2] #####
u =  -1.0
sqrt(1-u) =  1.4142135623730951
sqrt(1+u) =  0.0
v =  -1.5485281374238569
alpha =  -8.0
beta =  22.22260368491507
f(x) =  -1.7862236413056787e-09
##### Done. #####

e[ 1 ] =  [0.e+00 1.e-06]
f(x + e[ 1 ]) =  -1.7863573320035263e-09
f(x - e[ 1 ]) =  -1.7862236413056787e-09
***** Done. ***** 

***** my_grad_approx at x =  [-0.2      -0.200001] *****
##### my_f at x =  [-0.199999 -0.200001] #####
u =  -0.9999990000000001
sqrt(1-u) =  1.4142132088196604
sqrt(1+u) =  0.0009999999999588667
v =  -1.5485270282367944
alpha =  -7.973997052001044
beta =  22.222602582361898
f(x) =  -1.7804197192545872e-09
##### Done. #####

##### my_f at x =  [-0.200001 -0.200001] #####
u =  -1.0000010000000001
sqrt(1-u) =  1.4142139159264415
sqrt(1+u) =  nan
v =  -1.548531246613413
alpha =  nan
beta =  22.222679630328063
f(x) =  nan
##### Done. #####

e[ 0 ] =  [1.e-06 0.e+00]
f(x + e[ 0 ]) =  -1.7804197192545872e-09
f(x - e[ 0 ]) =  nan
##### my_f at x =  [-0.2 -0.2] #####
u =  -1.0
sqrt(1-u) =  1.4142135623730951
sqrt(1+u) =  0.0
v =  -1.5485281374238569
alpha =  -8.0
beta =  22.22260368491507
f(x) =  -1.7862236413056787e-09
##### Done. #####

##### my_f at x =  [-0.2      -0.200002] #####
u =  -1.0
sqrt(1-u) =  1.4142135623730951
sqrt(1+u) =  0.0
v =  -1.548530137423857
alpha =  -8.0
beta =  22.222678527720475
f(x) =  -1.7860899603198772e-09
##### Done. #####

e[ 1 ] =  [0.e+00 1.e-06]
f(x + e[ 1 ]) =  -1.7862236413056787e-09
f(x - e[ 1 ]) =  -1.7860899603198772e-09
***** Done. ***** 

[[            nan             nan]
 [            nan -2.42801152e-06]] 

/var/folders/b1/m1_w6rrd109bgym0_8fsxw_80000gp/T/ipykernel_92029/2873126429.py:19: RuntimeWarning: invalid value encountered in sqrt
  s2 = np.sqrt(1 + u)

Activity: Use 50I50\mathbf{I} in place of Hessian

Use the displayed constant model 50I50\mathbf I in place of the Hessian. This computes a gradient step with scale 1/501/50; it does not compute the objective’s actual curvature.

def eye_hes(x):
    return 50 * np.eye(len(x))

Test with starting point x=[0.7,0.3]T\mathbf{x}=[0.7,0.3]^T

# Specify starting point
x0 = np.array([0.7, 0.3])

# Create a Lambda (anonymous) function for gradient calculation
calc_grad = lambda x: my_grad_approx(x, my_f, 1e-6, verbose=False)

# Call optimization routine
x, f, p = alg1(x0, my_f, calc_grad, eye_hes, eps1, eps2, max_iter=25, verbose=False)

# Actual Hessian
print("Hessian at x* = ")
analyze_hes(my_hes_approx(x[-1], calc_grad, 1e-6))
Iter. 	f(x) 		||grad(x)|| 	||p|| 		min(lambda)
0   	-4.9246e+00 	1.0874e+01 	2.1749e-01 	 5.0000e+01
1   	-1.4795e+00 	1.6883e+01 	3.3765e-01 	 5.0000e+01
2   	-1.8403e+00 	2.6363e+01 	5.2725e-01 	 5.0000e+01
3   	-1.2461e-01 	1.6246e+00 	3.2493e-02 	 5.0000e+01
4   	-1.9169e-01 	2.5753e+00 	5.1507e-02 	 5.0000e+01
5   	-3.8498e-01 	5.2112e+00 	1.0422e-01 	 5.0000e+01
6   	-1.4594e+00 	1.6836e+01 	3.3673e-01 	 5.0000e+01
7   	-1.9249e+00 	2.6467e+01 	5.2934e-01 	 5.0000e+01
8   	-1.1531e-01 	1.5097e+00 	3.0195e-02 	 5.0000e+01
9   	-1.7247e-01 	2.3351e+00 	4.6703e-02 	 5.0000e+01
10   	-3.2703e-01 	4.5030e+00 	9.0059e-02 	 5.0000e+01
11   	-1.0837e+00 	1.3485e+01 	2.6971e-01 	 5.0000e+01
12   	-4.4497e+00 	2.0946e+01 	4.1891e-01 	 5.0000e+01
13   	-1.6236e-01 	2.1013e+00 	4.2026e-02 	 5.0000e+01
14   	-2.8114e-01 	3.6906e+00 	7.3812e-02 	 5.0000e+01
15   	-7.2912e-01 	9.1154e+00 	1.8231e-01 	 5.0000e+01
16   	-4.1506e+00 	2.1547e+01 	4.3094e-01 	 5.0000e+01
17   	-5.4382e-03 	2.2424e-01 	4.4848e-03 	 5.0000e+01
18   	-6.5332e-03 	2.6508e-01 	5.3017e-03 	 5.0000e+01
19   	-8.0846e-03 	3.2179e-01 	6.4357e-03 	 5.0000e+01
20   	-1.0414e-02 	4.0482e-01 	8.0965e-03 	 5.0000e+01
21   	-1.4199e-02 	5.3561e-01 	1.0712e-02 	 5.0000e+01
22   	-2.1098e-02 	7.6415e-01 	1.5283e-02 	 5.0000e+01
23   	-3.6112e-02 	1.2322e+00 	2.4644e-02 	 5.0000e+01
24   	-8.0386e-02 	2.4796e+00 	4.9592e-02 	 5.0000e+01
25   	-3.1729e-01 	7.8096e+00 	1.5619e-01 	 5.0000e+01
Maximum number of iterations.
Done.
x* =  [0.71010106 0.23551614]
Hessian at x* = 
[[ 45.2508031    7.93765054]
 [  7.93765054 147.99461656]] 

Eigenvalues:  [ 44.64118334+0.j 148.60423633+0.j] 

Test with starting point x=[0.0,0.0]T\mathbf{x}=[0.0,0.0]^T

# Specify starting point
x0 = np.array([0.0, 0.0])

# Call optimization routine
x, f, p = alg1(x0, my_f, calc_grad, calc_hes, eps1, eps2)

# Actual Hessian
print("Hessian at x*= ")
analyze_hes(my_hes_approx(x[-1], calc_grad, 1e-6))
Iter. 	f(x) 		||grad(x)|| 	||p|| 		min(lambda)
0   	 1.6212e-06 	1.5568e-04 	3.8590e-02 	-2.9496e-03
1   	 5.4102e-07 	5.5405e-05 	3.6202e-02 	-1.0900e-03
Done.
x* =  [-0.00124597 -0.07478209]
Hessian at x*= 
[[0.0001531  0.00055527]
 [0.00055527 0.00014766]] 

Eigenvalues:  [ 0.00070565+0.j -0.00040489+0.j] 

Adjust Hessian with Levenberg-Marquardt Correction

def adjusted_hes(x, grad, eps2, eps3, verbose=False):

    # Estimate Hessian with finite difference
    hes = my_hes_approx(x, grad, eps2)

    # Calculate eigenvalues
    l, V = linalg.eig(hes)
    smallest_ev = np.min(np.real(l))

    # Calculate modification
    delta = 0
    if smallest_ev - eps3 < 0:
        delta = -smallest_ev + eps3

    if verbose:
        print("Added ({0:1.4e})*I in LM correction.".format(delta))

    # Adjust hessian with Levenberg-Marquardt Correction
    return V.dot(np.diag(np.real(l)) + delta * np.eye(len(x))).dot(V.T)
# Specify starting point
x0 = np.array([-0.1, -0.1])

# Create a Lambda (anonymous) function for Hessian calculation
calc_hes = lambda x: adjusted_hes(x, calc_grad, 1e-6, 1e-3, verbose=True)

# Call optimization routine
x, f, p = alg1(x0, my_f, calc_grad, calc_hes, eps1, eps2, verbose=False)

# Actual Hessian
print("Hessian at x*= ")
analyze_hes(my_hes_approx(x[-1], calc_grad, 1e-6))
Iter. 	f(x) 		||grad(x)|| 	||p|| 		min(lambda)
Added (1.2030e-03)*I in LM correction.
0   	-2.0284e-07 	6.5946e-06 	6.5923e-03 	 1.0000e-03
Done.
x* =  [-0.09849776 -0.0935812 ]
Hessian at x*= 
[[ 4.20491243e-05 -5.31304857e-05]
 [-5.31304857e-05 -2.35437850e-04]] 

Eigenvalues:  [ 5.18741524e-05+0.j -2.45262878e-04+0.j] 

Enhancement Ideas

Here are some ideas to improve this notebook:

  • Plot the test function

  • On the plot of the test function, visualize the solution path