Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add algorithm for approximating the nth root with Newton's Method #11509

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions maths/numerical_analysis/nth_root.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""
Approximate the nth root of a real number using the Newton's Method.

The nth root of a real number R can be computed with Newton's method,
which starts with an initial guess x_0 and then iterates using the
recurrence relation:

x_{k + 1} = x_k - ((x_k)**n - R)/(n*(x_k)**(n-1))

The recurrence relation can be rewritten for computational efficiency:

x_{k + 1} = (n-1)/n*x_k + R/(n*(x_k)**(n-1))

Given a tolerance TOL, a stopping criterion can be set as:

abs(x_{k + 1} - x_k) < TOL

References:
- https://en.wikipedia.org/wiki/Nth_root#Using_Newton's_method
- Sauer, T. (2011): Numerical analysis.
USA. Addison-Wesley Publishing Company.
"""

from math import pow


def nth_root(radicand: float, index: int, tolerance: float = 0.0001) -> float:
"""
Approximate the nth root of the radicand for the given index

Args:
radicand: number from which the root is taken
index: positive integer which is the degree of the root
tolerance: positive real number that establishes the stopping criterion

Returns:
new_aproximation: approximation of the nth root of the radicand for the
given index

Raises:
TypeError: radicand is not real number
TypeError: index is not integer
ValueError: index is not positive integer
TypeError: tolerance is not real number
ValueError: tolerance is not positive real number
ValueError: math domain error

>>> round(nth_root(9, 2),1)
3.0

>>> int(round(nth_root(-8, 3, 0.001)))
-2

>>> int(round(nth_root(256, 4, 0.001)))
4

>>> round(nth_root(2, 2), 5)
1.41421

>>> round(nth_root(0.25, 2, 0.00000001), 1)
0.5

>>> round(nth_root(-8/27, 3, 0.0000001), 5)
-0.66667

>>> nth_root(0, 2, 0.1)
0.0

>>> nth_root(0.0, 5)
0.0

>>> all(abs(nth_root(k, k, 0.00000001) - k**(1/k)) <= 1e-10 for k in range(1,10))
True

>>> nth_root('invalid input', 3, 0.0001)
Traceback (most recent call last):
...
TypeError: radicand must be real number, not str

>>> nth_root(4, 0.5, 0.0001)
Traceback (most recent call last):
...
TypeError: index must be integer, not float

>>> nth_root(16, -4, 0.001)
Traceback (most recent call last):
...
ValueError: index must be positive integer, -4 <= 0

>>> nth_root(4, 2, '0.000001')
Traceback (most recent call last):
...
TypeError: tolerance must be real number, not str

>>> nth_root(9, 2, -0.01)
Traceback (most recent call last):
...
ValueError: tolerance must be positive real number, -0.01 <= 0

>>> nth_root(-256, 4, 0.0001)
Traceback (most recent call last):
...
ValueError: math domain error, radicand must be nonnegative for even index
"""
if not isinstance(radicand, (int, float)):
error_message = f"radicand must be real number, not {type(radicand).__name__}"
raise TypeError(error_message)

if not isinstance(index, int):
error_message = f"index must be integer, not {type(index).__name__}"
raise TypeError(error_message)

if index <= 0:
error_message = f"index must be positive integer, {index} <= 0"
raise ValueError(error_message)

if not isinstance(tolerance, (int, float)):
error_message = f"tolerance must be real number, not {type(tolerance).__name__}"
raise TypeError(error_message)

if tolerance <= 0:
error_message = f"tolerance must be positive real number, {tolerance} <= 0"
raise ValueError(error_message)

if radicand < 0 and index % 2 == 0:
error_message = "math domain error, radicand must be nonnegative for even index"
raise ValueError(error_message)

if radicand == 0.0:
return 0.0

# Set initial guess
new_aproximation = radicand
# Set old_aproximation to enter the loop
old_aproximation = new_aproximation + tolerance + 0.1

# Iterate as long as the stop criterion is not satisfied
while tolerance <= abs(old_aproximation - new_aproximation):
old_aproximation = new_aproximation
# Compute new_approximation with the recurrence relation described above
first_summand = (index - 1) / index * old_aproximation
second_summand = radicand / (index * pow(old_aproximation, index - 1))
new_aproximation = first_summand + second_summand

return new_aproximation


if __name__ == "__main__":
import doctest

doctest.testmod()