The NewtonRaphson method aka the Newton method is a rootfinding algorithm that approximates a root of a given realvalued function fx. It is an iterative method given by the formula xn  1  xn  fxn  f'xn with the precision of the approximation increasing as the number of iterations increase. Reference: https:en.wikipedia.orgwikiNewton27smethod Approximate the derivative of a function fx at a point x using the finite difference method  import math  tolerance  1e5  derivative  calcderivativelambda x: x2, 2  math.isclosederivative, 4, abstoltolerance True  derivative  calcderivativemath.sin, 0  math.isclosederivative, 1, abstoltolerance True Find a root of the given function f using the NewtonRaphson method. :param f: A realvalued singlevariable function :param x0: Initial guess :param maxiter: Maximum number of iterations :param step: Step size of x, used to approximate f'x :param maxerror: Maximum approximation error :param logsteps: bool denoting whether to log intermediate steps :return: A tuple containing the approximation, the error, and the intermediate steps. If logsteps is False, then an empty list is returned for the third element of the tuple. :raises ZeroDivisionError: The derivative approaches 0. :raises ArithmeticError: No solution exists, or the solution isn't found before the iteration limit is reached.  import math  tolerance  1e15  root,   newtonraphsonlambda x: x2  5x  2, 0.4, maxerrortolerance  math.iscloseroot, 5  math.sqrt17  2, abstoltolerance True  root,   newtonraphsonlambda x: math.logx  1, 2, maxerrortolerance  math.iscloseroot, math.e, abstoltolerance True  root,   newtonraphsonmath.sin, 1, maxerrortolerance  math.iscloseroot, 0, abstoltolerance True  newtonraphsonmath.cos, 0 Traceback most recent call last: ... ZeroDivisionError: No converging solution found, zero derivative  newtonraphsonlambda x: x2  1, 2 Traceback most recent call last: ... ArithmeticError: No converging solution found, iteration limit reached
from collections.abc import Callable

RealFunc = Callable[[float], float]


def calc_derivative(f: RealFunc, x: float, delta_x: float = 1e-3) -> float:
    """
    Approximate the derivative of a function f(x) at a point x using the finite
    difference method

    >>> import math
    >>> tolerance = 1e-5
    >>> derivative = calc_derivative(lambda x: x**2, 2)
    >>> math.isclose(derivative, 4, abs_tol=tolerance)
    True
    >>> derivative = calc_derivative(math.sin, 0)
    >>> math.isclose(derivative, 1, abs_tol=tolerance)
    True
    """
    return (f(x + delta_x / 2) - f(x - delta_x / 2)) / delta_x


def newton_raphson(
    f: RealFunc,
    x0: float = 0,
    max_iter: int = 100,
    step: float = 1e-6,
    max_error: float = 1e-6,
    log_steps: bool = False,
) -> tuple[float, float, list[float]]:
    """
    Find a root of the given function f using the Newton-Raphson method.

    :param f: A real-valued single-variable function
    :param x0: Initial guess
    :param max_iter: Maximum number of iterations
    :param step: Step size of x, used to approximate f'(x)
    :param max_error: Maximum approximation error
    :param log_steps: bool denoting whether to log intermediate steps

    :return: A tuple containing the approximation, the error, and the intermediate
        steps. If log_steps is False, then an empty list is returned for the third
        element of the tuple.

    :raises ZeroDivisionError: The derivative approaches 0.
    :raises ArithmeticError: No solution exists, or the solution isn't found before the
        iteration limit is reached.

    >>> import math
    >>> tolerance = 1e-15
    >>> root, *_ = newton_raphson(lambda x: x**2 - 5*x + 2, 0.4, max_error=tolerance)
    >>> math.isclose(root, (5 - math.sqrt(17)) / 2, abs_tol=tolerance)
    True
    >>> root, *_ = newton_raphson(lambda x: math.log(x) - 1, 2, max_error=tolerance)
    >>> math.isclose(root, math.e, abs_tol=tolerance)
    True
    >>> root, *_ = newton_raphson(math.sin, 1, max_error=tolerance)
    >>> math.isclose(root, 0, abs_tol=tolerance)
    True
    >>> newton_raphson(math.cos, 0)
    Traceback (most recent call last):
    ...
    ZeroDivisionError: No converging solution found, zero derivative
    >>> newton_raphson(lambda x: x**2 + 1, 2)
    Traceback (most recent call last):
    ...
    ArithmeticError: No converging solution found, iteration limit reached
    """

    def f_derivative(x: float) -> float:
        return calc_derivative(f, x, step)

    a = x0  # Set initial guess
    steps = []
    for _ in range(max_iter):
        if log_steps:  # Log intermediate steps
            steps.append(a)

        error = abs(f(a))
        if error < max_error:
            return a, error, steps

        if f_derivative(a) == 0:
            raise ZeroDivisionError("No converging solution found, zero derivative")
        a -= f(a) / f_derivative(a)  # Calculate next estimate
    raise ArithmeticError("No converging solution found, iteration limit reached")


if __name__ == "__main__":
    import doctest
    from math import exp, tanh

    doctest.testmod()

    def func(x: float) -> float:
        return tanh(x) ** 2 - exp(3 * x)

    solution, err, steps = newton_raphson(
        func, x0=10, max_iter=100, step=1e-6, log_steps=True
    )
    print(f"{solution=}, {err=}")
    print("\n".join(str(x) for x in steps))
