Reference: Sections 3.1 - 3.3 in Biegler (2010)
# Load required Python libraries.
import numpy as np
from scipy import linalg
# Seed the random number generator so this notebook is reproducible
# (Pyomo style guide, section 8).
np.random.seed(0)Unconstrained Optimization with Approximate Hessian¶
A Newton-type method uses a symmetric curvature model and solves . A positive-definite model gives a descent direction when the gradient is nonzero; an accepted update is . Unit-step modified Newton takes , but positive definiteness alone does not guarantee that this full step decreases the actual objective.
The examples below retain deliberate SR1 breakdowns. The detailed reference sections at the end explain the secant, signed update, curvature safeguards, and conditional rates. Biegler (2010), Algorithm 3.1 and §3.2, pp. 40–42.
For a differentiable objective and a nonzero gradient, a symmetric positive-definite model gives a descent direction. If its eigenvalues satisfy for all , the inverse Rayleigh bound gives
This proves a negative initial slope. Differentiability gives , so sufficiently small positive steps decrease the objective. A full step can still overshoot. A scalar eigenvalue shift changes each eigenvalue by the same amount while preserving the eigenvectors. To enforce a floor , the smallest nonnegative shift is . The diagram shows the shift geometry; the classroom example computes the shift from the original spectrum and the floor.


SR1 update is one way to approximate .
Library of helper functions¶
## Check if any element of array is NaN
def check_nan(A):
return np.sum(np.isnan(A))
## 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 Hessian using central finite difference
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
## Linear algebra calculation
def xxT(u):
"""
Calculates u*u.T to circumvent limitation with SciPy
Arguments:
u - numpy 1D array
Returns:
u*u.T
Assume u is a nx1 vector.
Recall: NumPy does not distinguish between row or column vectors
u.dot(u) returns a scalar. This function returns an nxn matrix.
"""
n = len(u)
A = np.zeros([n, n])
for i in range(0, n):
for j in range(0, n):
A[i, j] = u[i] * u[j]
return A
## Analyze Hessian
def analyze_hes(B):
print(B, "\n")
l = linalg.eigvals(B)
print("Eigenvalues: ", l, "\n")Symmetric Rank 1 (SR1) Update¶
def alg1_sr1(x0, calc_f, calc_grad, eps1, eps2, verbose=False, max_iter=1000):
"""
Arguments:
x0 - starting point
calc_f - function that calculates f(x)
calc_grad - function that calculates gradient(x)
Outputs:
x - iteration history of x (decision variables)
f - iteration history of f(x) (objective value)
p - iteration history of p (steps)
B - Hessian approximation
"""
# Allocate outputs as empty lists
x = []
f = []
p = []
grad = []
B = []
# 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 and k < max_iter:
# Evaluate f(x) at current iteration
f.append(calc_f(x[k]))
# Evaluate gradient
grad.append(calc_grad(x[k]))
if check_nan(grad[k]):
print("WARNING: gradient calculation returned NaN")
break
if verbose:
print("\n")
print("k = ", k)
print("x = ", x[k])
print("grad = ", grad[k])
# Update Hessian approximation
if k == 0:
# Initialize with identity
B.append(np.eye(len(x0)))
else:
# Change in x
s = x[k] - x[k - 1]
# Change in gradient
y = grad[k] - grad[k - 1]
# SR1 formulation
u = y - B[k - 1].dot(s)
denom = (u).dot(s)
# Safeguard: skip test for the SR1 update.
# Only apply the update when
# | (y - B^k s)^T s | >= C1 * ||s||^2, C1 > 0
# and otherwise leave B unchanged. See the "Quasi-Newton Methods"
# lecture notes, Strategy 1 (SR1), and Biegler (2010), p. 43.
# If the denominator is (near) zero the rank-one correction is an
# arbitrarily large, arbitrarily ill-conditioned change built from
# almost no new information. Note the test is two-sided: it looks at
# the magnitude only, because -- unlike BFGS -- SR1 is allowed to
# represent negative curvature.
C1 = 1e-8
if abs(denom) >= C1 * s.dot(s):
# Formula: dB = u * u.T / (u.T * s) if u is a column vector.
dB = xxT(u) / denom
skipped = False
else:
# Skip the update: B[k] = B[k-1]
dB = np.zeros((len(x0), len(x0)))
skipped = True
if verbose:
print("s = ", s)
print("y = ", y)
print("SR1 update denominator, (y-B[k-1]*s).T*s = ", denom)
print("SR1 update u = ", u)
if skipped:
print("Skip test |denom| >= C1*||s||^2 failed: update skipped.")
print("SR1 update u.T*u/(u.T*s) = \n", dB)
B.append(B[k - 1] + dB)
if verbose:
print("B = \n", B[k])
if check_nan(B[k]):
print("WARNING: Hessian update returned NaN")
break
c = np.linalg.cond(B[k])
if c > 1e12:
flag = False
print("Warning: Hessian approximation is near singular.")
print("B[k] = \n", B[k])
else:
# Calculate step
p.append(linalg.solve(B[k], -grad[k]))
if verbose:
print("p = ", p[k])
# Take step
x.append(x[k] + p[k])
# Calculate norms
norm_p = linalg.norm(p[k])
norm_g = linalg.norm(grad[k])
# Calculate eigenvalues of Hessian (for display only)
ev = np.real(linalg.eigvals(B[k]))
# 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
print("Done.")
print("x* = ", x[-1])
return x, f, p, B
print("Done.")Done.
def my_f_simple(x):
return x[0] ** 2 + (x[1] - 1) ** 2
def my_grad_exact(x):
return np.array([2 * x[0], 2 * (x[1] - 1)])Near solution¶
Consider
# 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_simple,1E-6,verbose=False)
calc_grad = lambda x: my_grad_exact(x)
# Specify starting point
x0 = np.array([-0.1, 0.2])
# Call optimization routine
x, f, p, B = alg1_sr1(
x0, my_f_simple, calc_grad, eps1, eps2, verbose=False, max_iter=50
)
# SR1 Hessian approximation
print("\nSR1 Hessian approximation. B[k] =")
analyze_hes(B[-1])
# Actual Hessian
print("True Hessian at x*. B =")
analyze_hes(my_hes_approx(x[-1], calc_grad, 1e-6))Iter. f(x) ||grad(x)|| ||p|| min(lambda)
0 6.5000e-01 1.6125e+00 1.6125e+00 1.0000e+00
1 6.5000e-01 1.6125e+00 8.0623e-01 1.0000e+00
2 1.9259e-34 2.7756e-17 2.7595e-17 1.0000e+00
Done.
x* = [-1.36642834e-17 1.00000000e+00]
SR1 Hessian approximation. B[k] =
[[1.01538462 0.12307692]
[0.12307692 1.98461538]]
Eigenvalues: [1.+0.j 2.+0.j]
True Hessian at x*. B =
[[2. 0.]
[0. 2.]]
Eigenvalues: [2.+0.j 2.+0.j]
Far from solution¶
Consider
# Specify starting point
x0 = np.array([-100, 500])
# Call optimization routine
x, f, p, B = alg1_sr1(
x0, my_f_simple, calc_grad, eps1, eps2, verbose=False, max_iter=50
)
# SR1 Hessian approximation
print("\nSR1 Hessian approximation. B[k] =")
analyze_hes(B[-1])
# Actual Hessian
print("True Hessian at x*. B =")
analyze_hes(my_hes_approx(x[-1], calc_grad, 1e-6))Iter. f(x) ||grad(x)|| ||p|| min(lambda)
0 2.5900e+05 1.0178e+03 1.0178e+03 1.0000e+00
1 2.5900e+05 1.0178e+03 5.0892e+02 1.0000e+00
2 3.4331e-27 1.1719e-13 7.2963e-14 1.0000e+00
Done.
x* = [2.46138186e-14 1.00000000e+00]
SR1 Hessian approximation. B[k] =
[[ 1.03860989 -0.19266335]
[-0.19266335 1.96139011]]
Eigenvalues: [1.+0.j 2.+0.j]
True Hessian at x*. B =
[[2. 0.]
[0. 2.]]
Eigenvalues: [2.+0.j 2.+0.j]
Activity/Discussion¶
Does the number of iterations depend on the starting point for this problem?
How many iterations are needed for Newton’s method to converge for a positive definite quadratic program using exact second derivative information?
Why does the SR1 update not converge to the true Hessian?
Test Case: Example 2.19¶
Biegler’s two-variable test problem is
with , , , and . We use , , and .
The reported solution is with . The cells below recompute it rather than relying on the printed result. (Biegler, Example 2.19, p. 30.)
## Define Python function to calculate objective
def my_f_ex_2_19(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 indicies 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# Create a Lambda (anonymous) function for gradient calculation
calc_grad = lambda x: my_grad_approx(x, my_f_ex_2_19, 1e-6, verbose=False)
# Specify starting point
x0 = np.array([0.3, 0.1])
# Call optimization routine
x, f, p, B = alg1_sr1(
x0, my_f_ex_2_19, calc_grad, eps1, eps2, verbose=False, max_iter=250
)
# SR1 Hessian approximation
print("\nSR1 Hessian approximation. B[k] =")
analyze_hes(B[-1])
# Actual Hessian
print("True Hessian at x*. B =")
analyze_hes(my_hes_approx(x[-1], calc_grad, 1e-6))Iter. f(x) ||grad(x)|| ||p|| min(lambda)
0 -3.6022e-02 8.3847e-01 8.3847e-01 1.0000e+00
1 -4.1276e-02 4.7785e-01 3.0223e-01 1.0000e+00
2 -2.2301e+00 2.1236e+01 3.0865e-01 -6.8856e+01
3 -4.1260e-02 4.4645e-01 1.3053e-01 -6.7478e+01
4 -1.0354e-01 1.1855e+00 1.5062e-01 -7.1805e+01
5 -3.7115e-02 3.9982e-01 1.3096e-02 -5.8783e+01
6 -3.4039e-02 3.6164e-01 5.9865e-02 -1.4365e+01
7 -2.2398e-02 2.0377e-01 4.2988e-02 -9.5209e+00
8 -1.7133e-02 1.1914e-01 4.2522e-02 -6.4394e+00
9 -1.4292e-02 5.7282e-02 2.4810e-02 -4.8015e+00
10 -1.3542e-02 2.5813e-02 1.0353e-02 -2.8612e+00
11 -1.3373e-02 1.0933e-02 8.4091e-03 -1.6117e+00
12 -1.3325e-02 7.7765e-04 7.5906e-04 -1.6030e+00
13 -1.3324e-02 1.7106e-05 1.4129e-05 -1.5761e+00
Done.
x* = [0.80557705 0.96556999]
SR1 Hessian approximation. B[k] =
[[-1.48333929 -0.21589311]
[-0.21589311 -1.07333658]]
Eigenvalues: [-1.57605489+0.j -0.98062097+0.j]
True Hessian at x*. B =
[[-1.47003807 -0.20601966]
[-0.20601966 -1.06561635]]
Eigenvalues: [-1.55650234+0.j -0.97915207+0.j]
# Specify starting point
x0 = np.array([-0.1, 0.2])
# Call optimization routine
x, f, p, B = alg1_sr1(
x0, my_f_ex_2_19, calc_grad, eps1, eps2, verbose=False, max_iter=250
)
# SR1 Hessian approximation
print("\nSR1 Hessian approximation. B[k] =")
analyze_hes(B[-1])
# Actual Hessian
print("True Hessian at x*. B =")
analyze_hes(my_hes_approx(x[-1], calc_grad, 1e-6))Iter. f(x) ||grad(x)|| ||p|| min(lambda)
0 -4.5540e-04 9.2580e-03 9.2580e-03 1.0000e+00
1 -5.4879e-04 1.0959e-02 5.9578e-02 -1.8395e-01
2 -1.5604e-04 3.4877e-03 2.7627e-02 -1.2568e-01
3 -8.3235e-05 1.9563e-03 3.5000e-02 -5.5390e-02
4 -3.6006e-05 9.0023e-04 2.9559e-02 -3.0011e-02
5 -1.7097e-05 4.4964e-04 2.9246e-02 -1.5078e-02
6 -7.9092e-06 2.1835e-04 2.7389e-02 -7.7836e-03
7 -3.7242e-06 1.0745e-04 2.6349e-02 -3.9664e-03
8 -1.7531e-06 5.2702e-05 2.5208e-02 -2.0268e-03
Done.
x* = [-0.11328951 -0.05033867]
SR1 Hessian approximation. B[k] =
[[ 1.00141633e+00 -4.82365648e-02]
[-4.82365648e-02 2.91945446e-04]]
Eigenvalues: [ 1.00373512+0.j -0.00202684+0.j]
True Hessian at x*. B =
[[ 6.99111878e-05 -2.18302890e-04]
[-2.18302890e-04 -7.02713991e-04]]
Eigenvalues: [ 0.00012733+0.j -0.00076013+0.j]
Activity/Discussion¶
Classify each candidate solution.
Is the SR1 approximation always positive definite?
Broyden update with Cholesky factorization¶
As part of Algorithms Homework 3, you will adapt this example and implement the BFGS (Broyden-Fletcher-Goldfarb-Shanno) update formula.
You may decide to use the Cholesky factorization of ,
to make your BFGS update more efficient. (This is not required). Let’s see how to do Cholesky factorization with SciPy.
## Create random P.D. and symmetric matrix
B_ = np.eye(3) + xxT(np.random.normal(0, 1, 3))
print("B = \n", B_, "\n")
## Perform Cholesky factorization.
# By default, lower=False and L is upper triangular. Either works here,
# but we prefer L to be lower triangular for convention.
L = linalg.cholesky(B_, lower=True)
print("L = \n", L, "\n")
## Reconstruct B
print("L*L.T = \n", L.dot(L.T), "\n")B =
[[4.11188068 0.70589826 1.72654504]
[0.70589826 1.16012579 0.39164906]
[1.72654504 0.39164906 1.95792804]]
L =
[[2.02777728 0. 0. ]
[0.3481143 1.01928515 0. ]
[0.85144708 0.09344604 1.10645097]]
L*L.T =
[[4.11188068 0.70589826 1.72654504]
[0.70589826 1.16012579 0.39164906]
[1.72654504 0.39164906 1.95792804]]
Reference: secant measurements and signed SR1¶
Use Euclidean vector norms and their induced matrix norm unless a weighted norm is specified.
For a nonzero accepted displacement, and . For on an open containing the full segment , the componentwise FTC gives the exact identity
motivates . It fixes the action on one direction, not every matrix entry. A symmetric matrix has free entries and a nonzero secant gives independent scalar equations. For example, , gives with free ; positive definiteness adds .
For , signed SR1 is
It has the form with and . A formula using only cannot represent negative denominators. Multiplying by cancels the denominator and gives .
To see why the zero-denominator case is an obstruction, suppose a nonzero symmetric rank-one correction satisfies . Then is parallel to and , so . Thus with cannot be repaired by that rank-one form.
There are three cases: if , keep the existing matrix; if but , no symmetric rank-one secant correction exists; if the denominator is small but nonzero, a large correction can amplify errors. Nocedal and Wright’s scale-aware safeguard accepts only if
Biegler gives the alternative threshold . Neither test enforces a positive sign; SR1 can represent indefinite curvature. A trust-region method can use that curvature, whereas a basic line-search step requires a descent direction.
Direction coverage also matters for recovering a Hessian. The precise nonlinear SR1 result uses uniform linear independence and its other convergence/safeguard hypotheses, not merely a different-looking sequence of steps. See Nocedal and Wright, Theorem 6.2, p. 149. Biegler’s short independence remark on p. 43 should be read with those hypotheses.
Sources: Biegler (2010), Eqs. (3.8)–(3.10), pp. 42–43; Nocedal and Wright (2006), §6.2, Eqs. (6.24) and (6.26), pp. 144–149.



Reference: BFGS, curvature safeguards, and implementation alternatives¶
With , and ,
Multiplication by gives . The correction has rank at most two. The positive curvature condition is necessary for any positive-definite secant matrix: .
A stronger quantitative test can reject small positive curvature as well as zero or negative curvature. It does not, by itself, bound the matrix norm. With , , and , the scalar test is always , but
has an arbitrarily large largest eigenvalue as grows. Additional bounds on measurements and updates matter.
Powell damping. Let and choose , commonly 0.2. Define . Taking
makes ; in the second case equality holds. The update then fits the damped measurement, not the original secant. The prepared picture below shows the interpolation and the permitted curvature half-space.
Inverse form. For , ,
For any nonzero , write . Then
If the second term is zero, , so the first term is positive. Otherwise the second term is positive. This proves positive definiteness without an eigenvalue decomposition. In exact arithmetic the inverse update equals the inverse of the direct BFGS update.
A dense inverse update still stores numbers and its matrix-vector step costs ; avoiding a solve is not avoiding a matrix. A Cholesky-factor update is another option: update the factor and use triangular solves instead of refactorizing a full new matrix. Finite precision still matters. For the existing Hilbert-matrix conditioning demonstration, see Linear Algebra Review and SciPy Basics, section D2. The example is not duplicated here. Positive definiteness maintained algebraically does not by itself establish safe conditioning in floating point.
Weighted least-change characterizations distinguish the updates: an appropriate weighted Frobenius formulation for the Hessian gives DFP; the corresponding inverse formulation gives BFGS. The norm and weighting hypotheses are essential (Nocedal and Wright, Eqs. (6.9)–(6.17), pp. 138–140). The broader Broyden family and its parameterization are discussed by Al-Baali, Spedicato and Maggioni (2014), pp. 942–943; BFGS is the zero-parameter member in their convention.
Sources: Biegler (2010), Eqs. (3.18)–(3.21), pp. 44–45, and the errata for Eq. (3.23) if using its alternative rank-two vectors; Nocedal and Wright (2006), pp. 137–141; Dennis and Schnabel (1996), Lemma 9.2.1, p. 199, and Eq. (9.2.10), p. 201. These are mathematical reference formulas; the BFGS programming exercise above remains an exercise.



Reference: directional accuracy and superlinear convergence¶
For a nonterminated convergent sequence with , Q-superlinear convergence means
Equivalently, . Finite termination requires no quotient. Linear and quadratic rates have their primary definitions in the Newton reference.

Let on an open neighborhood of , , and . Suppose unit-step iterates converge to , with symmetric positive-definite , and nonzero errors/steps before termination. Superlinear convergence is equivalent to
Using instead gives the same limit by Hessian continuity. The condition concerns the directions used. For , and , the directional error is zero despite .

Proof mechanism. Write , and . Hessian continuity and nondegeneracy give a bounded inverse and . A Lipschitz Hessian strengthens this to , but that stronger assumption is unnecessary for the equivalence.
Subtracting the two solves gives
Under directional accuracy, the difference is . Hence
Since , rearranging for gives
Conversely, superlinear convergence gives and thus . Subtracting the Newton-step remainder then gives . Multiply by the bounded and divide by to recover directional accuracy. Finite termination is handled separately; the displayed ratios describe a nonterminated sequence.
A strict minimum alone is insufficient. For with nonzero iterates, exact Newton has , so its directional error is identically zero, yet is only linear. Its limiting Hessian is zero. This exposes the missing nondegeneracy hypothesis in an unqualified reading of Biegler’s Theorem 3.1; Nocedal and Wright’s Theorem 3.7 states it explicitly.
For standard BFGS, the local superlinear theorem assumes convergence, a nondegenerate minimizer, the bound near for some , summable iterate errors, and the specified line-search/update framework. Wolfe searches test the unit step first and use a sufficient-decrease parameter strictly below when invoking the unit-step admissibility result. A finite gradient-norm plot does not prove those assumptions or a convergence rate. The shared Rosenbrock comparison below is one numerical experiment; its unmodified SR1 run deliberately stops on an ascent direction.
Sources: Nocedal and Wright (2006), Theorems 3.6–3.7, pp. 46–47; Assumption 6.2, p. 156, and Theorem 6.6, p. 158; Biegler (2010), Theorems 3.1–3.2, pp. 42 and 44, read with the explicit nondegenerate-minimum hypothesis.
Reference: storage and derivative-acquisition costs¶
Full dense BFGS stores values. Fixed-memory L-BFGS retains recent secant pairs, or values, plus workspace and base scaling. Its two-loop recursion computes the inverse action without forming an matrix (Nocedal and Wright, Algorithm 7.4, pp. 177–178). At float64 precision, , means 8 TB for one dense matrix versus 160 MB for the pairs, in decimal units. This is storage accounting, not a timing benchmark.
Discarding pairs means fixed-memory L-BFGS does not inherit the full BFGS superlinear theorem. Linear-rate results for limited memory impose additional smoothness, curvature and algorithm hypotheses; memory size alone establishes no universal rate. See Liu and Nocedal (1989), Algorithm 2.1, p. 506, and Theorem 7.1, p. 524, with the discussion on p. 526.
When only function values are available, derivative cost depends on the stencil and reuse policy. Forward differences reuse and require distinct points for a gradient. A complete forward second-difference Hessian also needs and for :
Including the reused points gives evaluations, or 861 at . A central Hessian stencil uses distinct points, or 3201 at ; its mixed entries use four sign combinations per pair. These counts assume a dense Hessian, shared points, and one common step size; sparsity, automatic derivatives, constraints, noise and domain restrictions change the practical choice. The earlier rough estimate was an order-of-growth comparison, not an exact call count.
BFGS avoids Hessian acquisition, but finite-difference gradients can have an error floor that prevents the exact-derivative asymptotic theorem from applying. The notebook’s intentionally nonfinite-derivative examples remain useful checks of domain restrictions.
Additional reading: Nocedal and Wright (2006), §8.1, pp. 195–203, on derivative approximation; Biegler (2010), §3.3, pp. 42–46, on quasi-Newton implementation. Factorization/error demonstrations are kept in the existing linear-algebra reference pages rather than copied here.

Shared Rosenbrock experiment: complete settings and outcomes¶
The handout comparison uses the existing canonical figure implementation. Its model is
The derivatives used by the experiment are
All methods use the same wolfe_step routine: test first, halve the trial value as needed, with and , and at most sixty trial reductions. The run records the gradient norm, uses threshold 10-11 and at most sixty accepted steps. On an exhausted search this demonstration routine returns its final trial; that returned value is not automatically a Wolfe certificate. These are finite experiment settings, not the hypotheses of an unconditional convergence theorem.
Newton uses the exact Hessian, adding if . BFGS begins with and updates only if . SR1 also starts from , permits either denominator sign, and stops if ; it has no curvature repair. Before searching, the implementation rejects a nonnegative slope for its BFGS/SR1 solve. Steepest descent uses .
| Method | Accepted steps | Final stored plot gradient norm | Outcome |
|---|---|---|---|
| Modified Newton | 22 | 1e-16 (display floor) | gradient threshold reached |
| BFGS | 37 | 1.82305e-13 | gradient threshold reached |
| SR1 | 3 | 14.1664 | ascent direction rejected |
| Steepest descent | 60 | 3.31254 | iteration cap reached |
The stored plot history floors values at 10-16 to support logarithmic display; it does not assert an exact derivative value of 10-16. The shared SR1 run’s negative-denominator update creates indefinite curvature (eigenvalues approximately -642 and 6); the next slope is approximately +0.26, so the descent-based routine rejects that direction. The norm trace remains finite and does not certify a minimum or an asymptotic rate. Source code, settings and this table are available together so the comparison can be reproduced without changing the implementation’s ownership.
