Lean Bisection Method
A polymorphic implementation of the bisection root-finding algorithm in Lean 4, supporting Float and ℝ.
Overview
The bisection method is a robust numerical algorithm for finding roots of continuous functions. This implementation provides:
- Polymorphic design via
BisectionFieldtypeclass - works with Float, ℚ, and ℝ - Configurable parameters (tolerance, max iterations (optional))
- Formal proofs of termination and boundedness (see
real_props.lean)
Core Components
-- Result type with detailed outcomes
inductive BisectionResult (α : Type*) where
| success (root : α) (iterations : Nat)
| invalidBounds (reason : String)
| noSignChange (reason : String)
| maxIterationsReached (bestApprox : α) (iterations : Nat)
-- Typeclass for numeric types supporting bisection using basic Mathlib definitions
class BisectionField (α : Type*) where
zero : α
two : α
add sub mul div : α → α → α
lt le : α → α → Bool
abs : α → α
isPositive isNegative : α → Bool
toFloat : α → Float -- for display
-- Configuration
structure BisectionParams (α : Type*) where
tolerance : α
maxIterations : Nat := 1000
-- Core algorithm (polymorphic)
def bisection {α : Type*} [BisectionField α]
(f : α → α) (a b : α) (params : BisectionParams α) : BisectionResult α
## Usage
### Running the Implementation
```bash
cd LeanBisection
lake build Bisection
Float Examples
-- Define test functions
def testSqrt2 (x : Float) : Float := x * x - 2.0
def testCubic (x : Float) : Float := x * x * x - x - 1.0
def testPi (x : Float) : Float := Float.sin x
-- Find roots with default parameters
#eval! findRoot testSqrt2 1.0 2.0 -- ✓ Root: 1.414214 (29 iterations)
#eval! findRoot testCubic 1.0 2.0 -- ✓ Root: 1.324718 (34 iterations)
#eval! findRoot testPi 3.0 4.0 -- ✓ Root: 3.141593 (33 iterations)
-- With custom configuration
def highPrecParams : BisectionParams Float := { tolerance := 1e-15, maxIterations := 200 }
#eval! bisection testSqrt2 1.0 2.0 highPrecParams -- ✓ Root: 1.414214 (50 iterations)
Float Limitations
Bisection can still "succeed" in cases where no true root exists due to floating-point behavior:
-- Pole crossing: 1/x on [-1, 1] has no root, but bisection finds x ≈ 0
def testPoleCross (x : Float) : Float := 1.0 / x
#eval! findRoot testPoleCross (-1.0) 1.0 -- ✓ Root: -0.000000 (no real root)
-- High powers: (x-1)^11 has root at x=1, but Float precision degrades
def testPower (x : Float) : Float := (x - 1)^11
#eval! findRoot testPower 0.9 1.5 -- not going to find exactly 1.0
Rational (ℚ) Examples
def testSqrt2Rat (x : ℚ) : ℚ := x * x - 2
#eval! findRootRat testSqrt2Rat 1 2 -- ✓ Root: 1.414214 (29 iterations)
Real (ℝ) Examples (noncomputable, for proofs)
noncomputable def testRealQuadratic : BisectionResult ℝ :=
bisection (fun x => x^2 - 2) 1 2 { tolerance := 1/1000000, maxIterations := 100 }
Error Handling
#eval! findRoot testSqrt2 2.0 1.0 -- ✗ Invalid bounds: left bound must be less than right bound
#eval! findRoot testSqrt2 2.0 3.0 -- ✗ No sign change: function must have opposite signs at bounds
Formal Proofs
The src/real_props.lean file contains formal proofs for the bisection algorithm:
- Interval halving: Each iteration halves the search interval
- Termination and boundedness: The result is always within [a, b]
theorem bisection_termination_bounded_real
(f : ℝ → ℝ) (a b tolerance : ℝ) (maxIter : ℕ)
(h_order : a < b) (h_tol_pos : 0 < tolerance) :
∃ x : ℝ, result = some x ∧ a ≤ x ∧ x ≤ b ∧
(∃ n : ℕ, n ≤ maxIter ∧ |x - ((a + b) / 2)| ≤ (b - a) / 2^(n + 1))
- Our proofs will be updated moving forward as the code grows
How It Works
- Validate bounds: Check that
a < bandf(a),f(b)have opposite signs - Iterate: Repeatedly bisect the interval at the midpoint
- Converge: Stop when interval width or |f(mid)| is below tolerance
Project Structure
LeanBisection/
├── src/
│ ├── bisection.lean # Core polymorphic implementation
│ ├── bisection_tests.lean # Test suite
│ └── real_props.lean # Formal proofs
├── lakefile.lean
└── README.md
Requirements
- Lean 4: v4.27.0-rc1 or compatible
- Mathlib
- Lake: