from source.code.unit11_numerical import bisection
result = bisection(lambda x: x*x - 2.0, 1.0, 2.0, width_tolerance=1e-12)
(result.midpoint, result.width, result.iterations)(1.414213562372879, 9.094947017729282e-13, 40)
After completing this unit, you will be able to:
This unit offers several routes. The root-finding route requires A30 and the concept of a continuous function. The quadrature route requires B30, the linear systems route requires B40, and the differential equations route requires B70. Defer any route whose prerequisites you have not yet met.
Only the sections on methods and theorems, A30 bisection, the SciPy comparison, and sources of error, together with Exercises 1 and 2 and SciPy exercise s01, form this unit’s B80 core. The B30, B40, and B70 routes remain available as extensions, but must not count as evidence of B80 completion before their respective prerequisites have been met.
A software command does not guarantee that a problem satisfies a method’s assumptions. Bisection, for example, maintains an interval on which the function has opposite signs at the two endpoints. Guaranteeing a root in that interval requires continuity. A program can check the endpoint signs; it cannot infer continuity from a few samples. This unit’s implementation also rejects nonfinite endpoints, midpoints, or function values.
Every numerical experiment states:
To find a root of , start with such that . Each step selects the midpoint and keeps the half-interval that still has a sign change.
If the initial width is , after steps the width is . This relationship provides an interval-based error bound, rather than relying only on a small value of .
from source.code.unit11_numerical import bisection
result = bisection(lambda x: x*x - 2.0, 1.0, 2.0, width_tolerance=1e-12)
(result.midpoint, result.width, result.iterations)(1.414213562372879, 9.094947017729282e-13, 40)
That value is an approximation. The exact statement comes from the definition of the positive root of , not from the printed digits.
The original implementation makes each bisection step readable. A second implementation, scipy.optimize.root_scalar, provides an independently developed comparison. Both take a specified function, initial interval, method, and iteration limit, but their tolerance parameters must not be treated as equivalent. original_width_tolerance bounds the final interval width in the original implementation; scipy_xtol and scipy_rtol specify absolute and relative closeness criteria for the root returned by SciPy. Their numerical values may coincide in this example, but their meanings remain distinct and are recorded separately.
from source.code.unit11_numerical import (
SCIPY_VERSION,
compare_bisection_with_scipy,
square_minus_two,
)
comparison_u11 = compare_bisection_with_scipy(
square_minus_two,
1.0,
2.0,
original_width_tolerance=1e-12,
scipy_xtol=1e-12,
)
assert comparison_u11["scipy_version"] == SCIPY_VERSION
assert comparison_u11["verification"]["strict_sign_change"]
assert comparison_u11["scipy"]["converged"]
assert comparison_u11["verification"]["roots_inside_initial_bracket"]
assert comparison_u11["verification"]["roots_agree"]
(
comparison_u11["scipy_version"],
comparison_u11["original"]["midpoint"],
comparison_u11["scipy"]["root"],
comparison_u11["verification"]["absolute_root_difference"],
)('1.15.2', 1.414213562372879, 1.4142135623724243, 4.547473508864641e-13)
The record stores the version of SciPy actually imported, not just the library name. strict_sign_change checks before the call. converged and flag record SciPy’s report. Both roots must remain inside the initial interval, and their difference must not exceed the recorded agreement tolerance.
Agreement between two implementations improves the chance of detecting local coding errors. It does not prove that the function is continuous or that the interval contains exactly one root, nor does it turn approximate digits into an exact value. Continuity and the existence guarantee rest on the assumptions and the Intermediate Value Theorem; the version and convergence checks provide computational evidence.
The quadrature section and Exercise 3 require B30. Both can be used for enrichment once that prerequisite has been met, but neither counts as B80 core content or evidence of B80 completion.
The composite trapezoidal rule replaces the curve on each subinterval with a line segment. A refinement experiment uses subintervals and records changes in the result. Values that appear stable support a conjecture of convergence, but do not replace error analysis.
For a linear function, the trapezoidal rule is exact in exact arithmetic. This case provides a stronger implementation check than an arbitrarily chosen reference number.
The linear systems section and Exercise 4 require B40. Neither counts as B80 core content or evidence of B80 completion.
If a program proposes as a solution of , compute the residual . A small residual means that the equation is nearly satisfied on the scale being used. A small residual does not always mean a small error in the solution; the relationship depends on the matrix’s conditioning.
For a two-by-two system with rational coefficients, an exact solution using Fraction can provide an independent comparison for the floating-point route.
The differential equations section and Exercise 5 require B70. Neither counts as B80 core content or evidence of B80 completion.
For the problem , an Euler step uses
Reducing and comparing with a known solution can reveal an error pattern in the example. This tests the implementation and the behavior of that case; it does not prove an order of convergence for every function .
When a result differs from expectations, distinguish:
Changing a tolerance addresses only some of these sources. A report must therefore not label a single number “numerical error” without defining it.
Run the local examples whose prerequisites you have met:
python source/code/unit11_numerical.py --output output/unit11-results.json
The output records the final bisection interval, the quadrature refinement table, system solutions and residuals, and Euler-step errors. The function identities, domains or initial data, tolerances, refinement grids, step counts, and reference values used by the checks are stored alongside the results.
The record also includes the SciPy comparison, the SciPy version, interval checks, convergence, agreement between results, and the limits of the evidence. The quadrature, linear systems, and Euler tables are retained as extension regression checks; their presence in the JSON does not make them part of the B80 core. The curriculum_routes field makes this separation explicit.
Name two invariants that should be checked at each bisection step, and explain why the midpoint value alone is insufficient.
Consider the sign change and the shrinking interval.
The interval endpoints must continue to bracket a sign change, and the width must halve at each step. The midpoint value alone does not establish that a root remains bracketed or that the implementation selected the correct half.
What is the minimum number of steps guaranteeing an interval width of at most if the initial width is 1?
Find such that .
Since and , 20 steps are needed. After 20 steps, the width is .
Use compare_bisection_with_scipy to find a root of on with a width tolerance of . Verify the sign change, SciPy version, convergence report, inclusion of both results in the interval, and agreement between the roots. Explain one matter that still requires a mathematical argument.
Define the Python function first. The comparison result has fields named endpoint_values, scipy_version, scipy, original, and verification.
from source.code.unit11_numerical import SCIPY_VERSION, compare_bisection_with_scipy
def cubic_function_u11(x):
return x**3 - x - 2
cubic_result_u11 = compare_bisection_with_scipy(
cubic_function_u11,
1.0,
2.0,
original_width_tolerance=1e-10,
scipy_xtol=1e-10,
)
assert cubic_result_u11["endpoint_values"][0] < 0
assert cubic_result_u11["endpoint_values"][1] > 0
assert cubic_result_u11["scipy_version"] == SCIPY_VERSION
assert cubic_result_u11["scipy"]["converged"]
assert cubic_result_u11["verification"]["roots_inside_initial_bracket"]
assert cubic_result_u11["verification"]["roots_agree"]
assert (
cubic_result_u11["verification"]["absolute_root_difference"]
<= cubic_result_u11["verification"]["agreement_tolerance"]
)Solution 14.1.
from source.code.unit11_numerical import SCIPY_VERSION, compare_bisection_with_scipy
def solution_cubic_function_u11(x):
return x**3 - x - 2
cubic_solution_u11 = compare_bisection_with_scipy(
solution_cubic_function_u11,
1.0,
2.0,
original_width_tolerance=1e-10,
scipy_xtol=1e-10,
)
cubic_verification_u11 = cubic_solution_u11["verification"]
assert cubic_solution_u11["method"] == "bisect"
assert cubic_solution_u11["scipy_version"] == SCIPY_VERSION
assert cubic_solution_u11["scipy"]["flag"] == "converged"
assert cubic_verification_u11["strict_sign_change"]
assert cubic_verification_u11["roots_inside_initial_bracket"]
assert cubic_verification_u11["roots_agree"]Since and , the endpoint values have opposite signs. Assuming that is continuous, the Intermediate Value Theorem guarantees at least one root in the interval. The two implementations agree within the recorded tolerance, and SciPy reports convergence. However, the two programs prove neither continuity nor uniqueness of the root; both require separate mathematical arguments.
Curriculum status: deferred B30 extension; not B80 core.
Apply the trapezoidal rule to on with one subinterval. Compare with the exact integral.
The trapezoid’s area is its width times the average endpoint height.
The endpoint values are 2 and 14, so the trapezoid’s area is . The exact integral is . This exact agreement is expected because the graph of a linear function on the interval is itself a line segment.
Curriculum status: deferred B40 extension; not B80 core.
Explain why a small residual does not automatically guarantee that an approximate solution is close to the exact solution.
Consider a system that is highly sensitive to small changes in the right-hand side.
The residual measures how well satisfies the given equation. For an ill-conditioned matrix, a small change in the right-hand side can correspond to a large change in the solution. A residual must therefore be interpreted alongside scale and conditioning information, not directly as the error in the solution.
Curriculum status: deferred B70 extension; not B80 core.
For , , compare the errors at for and . What conclusions are justified?
Use the reference solution , and do not turn two cases into a theorem.
Euler’s method gives . For the value is approximately 2.5937; for it is approximately 2.6533; whereas . The error decreases in these two cases. This supports the expected behavior and checks the implementation, but two step sizes do not prove convergence or an error order in general.
scipy.optimize.root_scalar(method="bisect") provide an A30 cross-check that must record the version, interval, tolerances, convergence, and agreement between results.