Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Ok-landscape
GitHub Repository: Ok-landscape/computational-pipeline
Path: blob/main/latex-templates/templates/numerical-methods/root_finding.tex
51 views
unlisted
1
\documentclass[a4paper, 11pt]{report}
2
\usepackage[utf8]{inputenc}
3
\usepackage[T1]{fontenc}
4
\usepackage{amsmath, amssymb}
5
\usepackage{graphicx}
6
\usepackage{siunitx}
7
\usepackage{booktabs}
8
\usepackage{xcolor}
9
\usepackage[makestderr]{pythontex}
10
11
\definecolor{bisect}{RGB}{231, 76, 60}
12
\definecolor{newton}{RGB}{46, 204, 113}
13
\definecolor{secant}{RGB}{52, 152, 219}
14
\definecolor{brent}{RGB}{155, 89, 182}
15
16
\title{Root Finding Algorithms:\\
17
Convergence Analysis and Comparison}
18
\author{Department of Computational Mathematics\\Technical Report NM-2024-003}
19
\date{\today}
20
21
\begin{document}
22
\maketitle
23
24
\begin{abstract}
25
This report presents a comprehensive analysis of root-finding algorithms for nonlinear equations. We implement and compare bisection, Newton-Raphson, secant, and Brent's methods. Convergence rates are analyzed theoretically and verified computationally. The importance of initial guesses and potential pitfalls of each method are demonstrated through examples.
26
\end{abstract}
27
28
\tableofcontents
29
30
\chapter{Introduction}
31
32
Root finding seeks solutions to $f(x) = 0$. We classify methods by their convergence rate:
33
\begin{itemize}
34
\item \textbf{Linear}: $|e_{n+1}| \leq C|e_n|$, where $C < 1$
35
\item \textbf{Superlinear}: $|e_{n+1}| \leq C|e_n|^p$, $1 < p < 2$
36
\item \textbf{Quadratic}: $|e_{n+1}| \leq C|e_n|^2$
37
\end{itemize}
38
39
\chapter{Bisection Method}
40
41
\section{Algorithm}
42
Given $f(a) \cdot f(b) < 0$:
43
\begin{enumerate}
44
\item Compute midpoint: $c = \frac{a + b}{2}$
45
\item If $f(a) \cdot f(c) < 0$, set $b = c$; else set $a = c$
46
\item Repeat until $|b - a| < \epsilon$
47
\end{enumerate}
48
49
Convergence: Linear, $|e_{n+1}| = \frac{1}{2}|e_n|$
50
51
\begin{pycode}
52
import numpy as np
53
import matplotlib.pyplot as plt
54
plt.rc('text', usetex=True)
55
plt.rc('font', family='serif')
56
57
np.random.seed(42)
58
59
def bisection(f, a, b, tol=1e-10, max_iter=100):
60
history = []
61
fa, fb = f(a), f(b)
62
63
if fa * fb > 0:
64
return None, []
65
66
for i in range(max_iter):
67
c = (a + b) / 2
68
fc = f(c)
69
history.append(c)
70
71
if abs(fc) < tol or (b - a) / 2 < tol:
72
return c, history
73
74
if fa * fc < 0:
75
b, fb = c, fc
76
else:
77
a, fa = c, fc
78
79
return c, history
80
81
def newton(f, df, x0, tol=1e-10, max_iter=100):
82
history = [x0]
83
x = x0
84
85
for i in range(max_iter):
86
fx = f(x)
87
dfx = df(x)
88
89
if abs(dfx) < 1e-14:
90
break
91
92
x_new = x - fx / dfx
93
history.append(x_new)
94
95
if abs(x_new - x) < tol:
96
return x_new, history
97
98
x = x_new
99
100
return x, history
101
102
def secant(f, x0, x1, tol=1e-10, max_iter=100):
103
history = [x0, x1]
104
105
for i in range(max_iter):
106
fx0, fx1 = f(x0), f(x1)
107
108
if abs(fx1 - fx0) < 1e-14:
109
break
110
111
x2 = x1 - fx1 * (x1 - x0) / (fx1 - fx0)
112
history.append(x2)
113
114
if abs(x2 - x1) < tol:
115
return x2, history
116
117
x0, x1 = x1, x2
118
119
return x1, history
120
121
def brent(f, a, b, tol=1e-10, max_iter=100):
122
fa, fb = f(a), f(b)
123
history = []
124
125
if fa * fb > 0:
126
return None, []
127
128
if abs(fa) < abs(fb):
129
a, b = b, a
130
fa, fb = fb, fa
131
132
c = a
133
fc = fa
134
mflag = True
135
d = 0
136
137
for i in range(max_iter):
138
history.append(b)
139
140
if abs(fb) < tol or abs(b - a) < tol:
141
return b, history
142
143
if fa != fc and fb != fc:
144
# Inverse quadratic interpolation
145
s = (a * fb * fc / ((fa - fb) * (fa - fc)) +
146
b * fa * fc / ((fb - fa) * (fb - fc)) +
147
c * fa * fb / ((fc - fa) * (fc - fb)))
148
else:
149
# Secant
150
s = b - fb * (b - a) / (fb - fa)
151
152
# Conditions to accept s
153
cond1 = (s < (3*a + b) / 4 or s > b)
154
cond2 = mflag and abs(s - b) >= abs(b - c) / 2
155
cond3 = not mflag and abs(s - b) >= abs(c - d) / 2
156
cond4 = mflag and abs(b - c) < tol
157
cond5 = not mflag and abs(c - d) < tol
158
159
if cond1 or cond2 or cond3 or cond4 or cond5:
160
s = (a + b) / 2
161
mflag = True
162
else:
163
mflag = False
164
165
fs = f(s)
166
d = c
167
c = b
168
fc = fb
169
170
if fa * fs < 0:
171
b, fb = s, fs
172
else:
173
a, fa = s, fs
174
175
if abs(fa) < abs(fb):
176
a, b = b, a
177
fa, fb = fb, fa
178
179
return b, history
180
\end{pycode}
181
182
\section{Comparison of Methods}
183
184
\begin{pycode}
185
# Test function: x^3 - x - 2
186
def f1(x):
187
return x**3 - x - 2
188
189
def df1(x):
190
return 3*x**2 - 1
191
192
# True root
193
true_root = 1.5213797068045676
194
195
# Apply all methods
196
root_bis, hist_bis = bisection(f1, 1, 2)
197
root_new, hist_new = newton(f1, df1, 1.5)
198
root_sec, hist_sec = secant(f1, 1, 2)
199
root_brent, hist_brent = brent(f1, 1, 2)
200
201
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
202
203
# Visualize bisection
204
ax = axes[0, 0]
205
x = np.linspace(0.5, 2.5, 200)
206
ax.plot(x, f1(x), 'b-', linewidth=2, label='$f(x) = x^3 - x - 2$')
207
ax.axhline(0, color='k', linestyle='-', alpha=0.3)
208
for i, xi in enumerate(hist_bis[:6]):
209
ax.axvline(xi, color='red', alpha=0.3 + 0.1*i, linestyle='--')
210
ax.scatter([true_root], [0], color='green', s=100, zorder=5, label=f'Root: {true_root:.6f}')
211
ax.set_xlabel('$x$')
212
ax.set_ylabel('$f(x)$')
213
ax.set_title('Bisection Method')
214
ax.legend()
215
ax.grid(True, alpha=0.3)
216
217
# Newton iterations
218
ax = axes[0, 1]
219
ax.plot(x, f1(x), 'b-', linewidth=2)
220
ax.axhline(0, color='k', linestyle='-', alpha=0.3)
221
for i in range(min(4, len(hist_new)-1)):
222
xi = hist_new[i]
223
yi = f1(xi)
224
slope = df1(xi)
225
x_tang = np.linspace(xi - 0.5, xi + 0.5, 50)
226
y_tang = yi + slope * (x_tang - xi)
227
ax.plot(x_tang, y_tang, 'g--', alpha=0.5)
228
ax.scatter([xi], [yi], color='green', s=50)
229
ax.scatter([true_root], [0], color='red', s=100, zorder=5)
230
ax.set_xlabel('$x$')
231
ax.set_ylabel('$f(x)$')
232
ax.set_title('Newton-Raphson Method')
233
ax.set_xlim(0.5, 2.5)
234
ax.set_ylim(-5, 10)
235
ax.grid(True, alpha=0.3)
236
237
# Convergence comparison
238
ax = axes[1, 0]
239
errors_bis = [abs(h - true_root) for h in hist_bis]
240
errors_new = [abs(h - true_root) for h in hist_new]
241
errors_sec = [abs(h - true_root) for h in hist_sec]
242
errors_brent = [abs(h - true_root) for h in hist_brent]
243
244
ax.semilogy(range(len(errors_bis)), errors_bis, 'ro-', label='Bisection')
245
ax.semilogy(range(len(errors_new)), errors_new, 'gs-', label='Newton')
246
ax.semilogy(range(len(errors_sec)), errors_sec, 'b^-', label='Secant')
247
ax.semilogy(range(len(errors_brent)), errors_brent, 'mp-', label='Brent')
248
ax.set_xlabel('Iteration')
249
ax.set_ylabel('$|x_n - x^*|$')
250
ax.set_title('Convergence Comparison')
251
ax.legend()
252
ax.grid(True, alpha=0.3)
253
254
# Convergence order estimation
255
ax = axes[1, 1]
256
# Newton convergence order
257
if len(errors_new) > 3:
258
log_e = np.log(errors_new[1:-1])
259
log_e_next = np.log(errors_new[2:])
260
log_e_prev = np.log(errors_new[:-2])
261
orders = log_e_next / log_e
262
ax.plot(range(len(orders)), orders, 'gs-', label='Newton')
263
264
# Secant convergence order
265
if len(errors_sec) > 3:
266
log_e = np.log(errors_sec[1:-1])
267
log_e_next = np.log(errors_sec[2:])
268
orders = log_e_next / log_e
269
ax.plot(range(len(orders)), orders, 'b^-', label='Secant')
270
271
ax.axhline(2, color='green', linestyle='--', alpha=0.5, label='Quadratic')
272
ax.axhline(1.618, color='blue', linestyle='--', alpha=0.5, label='Golden ratio')
273
ax.axhline(1, color='red', linestyle='--', alpha=0.5, label='Linear')
274
ax.set_xlabel('Iteration')
275
ax.set_ylabel('Estimated Order')
276
ax.set_title('Convergence Order')
277
ax.legend()
278
ax.grid(True, alpha=0.3)
279
ax.set_ylim(0, 3)
280
281
plt.tight_layout()
282
plt.savefig('root_finding_comparison.pdf', dpi=150, bbox_inches='tight')
283
plt.close()
284
\end{pycode}
285
286
\begin{figure}[htbp]
287
\centering
288
\includegraphics[width=0.95\textwidth]{root_finding_comparison.pdf}
289
\caption{Root finding methods: (a) bisection visualization, (b) Newton tangent lines, (c) error convergence, (d) convergence order estimation.}
290
\end{figure}
291
292
\chapter{Newton-Raphson Method}
293
294
\section{Algorithm}
295
\begin{equation}
296
x_{n+1} = x_n - \frac{f(x_n)}{f'(x_n)}
297
\end{equation}
298
299
Convergence: Quadratic near root (when $f'(x^*) \neq 0$)
300
301
\section{Pitfalls}
302
303
\begin{pycode}
304
# Newton failures
305
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
306
307
# Bad initial guess
308
ax = axes[0]
309
def f_bad(x):
310
return x**3 - 2*x + 2
311
312
def df_bad(x):
313
return 3*x**2 - 2
314
315
x = np.linspace(-2, 2, 200)
316
ax.plot(x, f_bad(x), 'b-', linewidth=2)
317
ax.axhline(0, color='k', alpha=0.3)
318
319
# Newton from x0=0 fails (derivative near 0)
320
x0 = 0.5
321
history = [x0]
322
xn = x0
323
for _ in range(5):
324
xn = xn - f_bad(xn) / df_bad(xn)
325
history.append(xn)
326
ax.scatter(history, [f_bad(h) for h in history], c=range(len(history)), cmap='Reds', s=50)
327
ax.set_xlabel('$x$')
328
ax.set_ylabel('$f(x)$')
329
ax.set_title('Newton Cycling')
330
ax.grid(True, alpha=0.3)
331
332
# Multiple roots
333
ax = axes[1]
334
def f_mult(x):
335
return (x - 1)**2
336
337
def df_mult(x):
338
return 2*(x - 1)
339
340
x = np.linspace(-1, 3, 200)
341
ax.plot(x, f_mult(x), 'b-', linewidth=2)
342
ax.axhline(0, color='k', alpha=0.3)
343
root_mult, hist_mult = newton(f_mult, df_mult, 3.0, max_iter=20)
344
errors_mult = [abs(h - 1) for h in hist_mult]
345
ax.scatter(hist_mult, [f_mult(h) for h in hist_mult], c=range(len(hist_mult)), cmap='Greens', s=30)
346
ax.set_xlabel('$x$')
347
ax.set_ylabel('$f(x)$')
348
ax.set_title('Multiple Root (Linear Convergence)')
349
ax.grid(True, alpha=0.3)
350
351
# Slow convergence for multiple roots
352
ax = axes[2]
353
ax.semilogy(range(len(errors_mult)), errors_mult, 'go-')
354
ax.set_xlabel('Iteration')
355
ax.set_ylabel('$|x_n - 1|$')
356
ax.set_title('Slow Convergence for $(x-1)^2 = 0$')
357
ax.grid(True, alpha=0.3)
358
359
plt.tight_layout()
360
plt.savefig('newton_pitfalls.pdf', dpi=150, bbox_inches='tight')
361
plt.close()
362
363
n_iterations = {
364
'Bisection': len(hist_bis),
365
'Newton': len(hist_new),
366
'Secant': len(hist_sec),
367
'Brent': len(hist_brent)
368
}
369
\end{pycode}
370
371
\begin{figure}[htbp]
372
\centering
373
\includegraphics[width=0.95\textwidth]{newton_pitfalls.pdf}
374
\caption{Newton method pitfalls: cycling, slow convergence for multiple roots.}
375
\end{figure}
376
377
\chapter{Secant Method}
378
379
\section{Algorithm}
380
\begin{equation}
381
x_{n+1} = x_n - f(x_n)\frac{x_n - x_{n-1}}{f(x_n) - f(x_{n-1})}
382
\end{equation}
383
384
Convergence: Superlinear, $p = \frac{1 + \sqrt{5}}{2} \approx 1.618$ (golden ratio)
385
386
Advantage: No derivative required.
387
388
\chapter{Brent's Method}
389
390
Combines bisection, secant, and inverse quadratic interpolation. Guaranteed convergence with superlinear speed for smooth functions.
391
392
\chapter{Numerical Results}
393
394
\begin{pycode}
395
results_data = [
396
('Bisection', n_iterations['Bisection'], 'Linear (1)', 'Guaranteed'),
397
('Newton', n_iterations['Newton'], 'Quadratic (2)', 'Requires $f\'$'),
398
('Secant', n_iterations['Secant'], 'Superlinear (1.618)', 'No derivative'),
399
('Brent', n_iterations['Brent'], 'Superlinear', 'Robust'),
400
]
401
\end{pycode}
402
403
\begin{table}[htbp]
404
\centering
405
\caption{Comparison for $f(x) = x^3 - x - 2$}
406
\begin{tabular}{@{}llll@{}}
407
\toprule
408
Method & Iterations & Order & Notes \\
409
\midrule
410
\py{results_data[0][0]} & \py{results_data[0][1]} & \py{results_data[0][2]} & \py{results_data[0][3]} \\
411
\py{results_data[1][0]} & \py{results_data[1][1]} & \py{results_data[1][2]} & \py{results_data[1][3]} \\
412
\py{results_data[2][0]} & \py{results_data[2][1]} & \py{results_data[2][2]} & \py{results_data[2][3]} \\
413
\py{results_data[3][0]} & \py{results_data[3][1]} & \py{results_data[3][2]} & \py{results_data[3][3]} \\
414
\bottomrule
415
\end{tabular}
416
\end{table}
417
418
\chapter{Multiple Test Functions}
419
420
\begin{pycode}
421
# Additional test functions
422
test_funcs = [
423
(lambda x: np.cos(x) - x, lambda x: -np.sin(x) - 1, 0, 1, "\\cos(x) - x"),
424
(lambda x: np.exp(x) - 3*x, lambda x: np.exp(x) - 3, 0, 2, "e^x - 3x"),
425
(lambda x: x**2 - 2, lambda x: 2*x, 1, 2, "x^2 - 2"),
426
]
427
428
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
429
430
for i, (f, df, a, b, name) in enumerate(test_funcs):
431
ax = axes[i]
432
x = np.linspace(a - 0.5, b + 0.5, 200)
433
ax.plot(x, f(x), 'b-', linewidth=2)
434
ax.axhline(0, color='k', alpha=0.3)
435
436
root_n, hist_n = newton(f, df, (a + b) / 2)
437
root_b, hist_b = bisection(f, a, b)
438
439
ax.scatter([root_n], [0], color='red', s=100, zorder=5, label=f'Root: {root_n:.6f}')
440
ax.set_xlabel('$x$')
441
ax.set_ylabel('$f(x)$')
442
ax.set_title(f'$f(x) = {name}$')
443
ax.legend()
444
ax.grid(True, alpha=0.3)
445
446
plt.tight_layout()
447
plt.savefig('multiple_functions.pdf', dpi=150, bbox_inches='tight')
448
plt.close()
449
\end{pycode}
450
451
\begin{figure}[htbp]
452
\centering
453
\includegraphics[width=0.95\textwidth]{multiple_functions.pdf}
454
\caption{Root finding for various test functions.}
455
\end{figure}
456
457
\chapter{Conclusions}
458
459
\begin{enumerate}
460
\item Bisection: Always converges but slow (linear)
461
\item Newton: Fast (quadratic) but requires derivative and good initial guess
462
\item Secant: Superlinear without derivatives
463
\item Brent: Best general-purpose method (robust + fast)
464
\item Multiple roots reduce Newton to linear convergence
465
\item Choice depends on: derivative availability, robustness needs, speed requirements
466
\end{enumerate}
467
468
\end{document}
469
470