from ...forall import *
####################################################################################
def bisection_method(f, a, b, eps=1e-6, max_iter=1000):
    """
    Метод дихотомии (половинного деления) для нахождения корня нелинейного уравнения.

    Теоретическая часть:
    Метод дихотомии основан на последовательном делении интервала [a, b] пополам и выборе подынтервала, 
    где функция меняет знак. Это гарантирует сходимость к корню при условии, что функция непрерывна 
    на начальном интервале и принимает значения разных знаков на его концах [[2]](https://en.wikipedia.org/wiki/Bisection_method).

    Практическая реализация:
    - Проверка корректности начального интервала (f(a)*f(b) < 0).
    - Итеративное деление интервала пополам с обновлением границ на основе знака функции.
    - Остановка при достижении заданной точности (eps) или максимального числа итераций (max_iter).

    Parameters
    ----------
    f : callable
        Непрерывная функция f(x), для которой ищется корень.
    a : float
        Левая граница начального интервала локализации корня.
    b : float
        Правая граница начального интервала локализации корня.
    eps : float, optional
        Точность вычисления корня (по значению функции или длине интервала). По умолчанию 1e-6.
    max_iter : int, optional
        Максимальное количество итераций для поиска корня. По умолчанию 1000.

    Returns
    -------
    float or None
        Приближенное значение корня, если метод сошелся.
        None, если достигнуто максимальное количество итераций без сходимости.

    Examples
    --------
    >>> f = lambda x: x**2 - 4
    >>> bisection_method(f, 1, 3)
    2.0
    
    >>> f = lambda x: math.sin(x)
    >>> bisection_method(f, 3, 4)
    3.141592025756836

    Notes
    -----
    1. Метод гарантированно находит корень, если функция f(x) непрерывна на [a, b] 
       и принимает значения разных знаков на концах интервала (f(a)*f(b) < 0) [[3]](https://example.com/dichotomy-method).
    2. Скорость сходимости линейная, с уменьшением интервала вдвое на каждой итерации.
    3. Критерий остановки: |f(c)| < eps или |b - a| < eps.
    4. При достижении максимального числа итераций возвращается None, 
       что требует дополнительной проверки результата.

    References
    ----------
    .. [1] "Метод половинного деления (метод дихотомии)" — Википедия
    .. [2] "Метод бисекции — Википедия", https://en.wikipedia.org/wiki/Bisection_method
    .. [3] "1.3 Метод половинного деления (метод дихотомии)"
    """
    if f(a) * f(b) >= 0:
        raise ValueError("Функция не меняет знак на интервале [a, b]")

    for _ in range(max_iter):
        c = (a + b) / 2
        if abs(f(c)) < eps or (b - a) < eps:
            return c
        if f(a) * f(c) < 0:
            b = c
        else:
            a = c
    return None
####################################################################################
def bisection_method_params(f, a, b, tol, params, max_iters=10000):
    """
    Нахождение корня функции f(x) на интервале [a, b] методом бисекции.
    В функцию можно вставить дополнительные параметры

    Теоретическая часть:
    Метод бисекции (метод половинного деления) основан на теореме о промежуточном значении для непрерывных функций. 
    Он гарантирует сходимость к корню, если функция f(x) непрерывна на [a, b] и принимает значения разных знаков на концах интервала [[2]](https://pythonnumericalmethods.berkeley.edu/).
    Метод последовательно делит интервал пополам и выбирает подынтервал, где функция меняет знак, обеспечивая линейную сходимость с уменьшением длины интервала вдвое на каждой итерации [[9]](https://docs.scipy.org/doc/scipy/reference/optimize.html).

    Практическая реализация:
    - Проверка корректности начального интервала (f(a) и f(b) имеют разные знаки).
    - Итеративное деление интервала пополам с обновлением границ на основе знака функции в середине.
    - Остановка при достижении заданной точности (|f(m)| < tol) или максимального числа итераций (max_iters).
    - Поддержка дополнительных параметров функции через кортеж `params`.

    Parameters
    ----------
    f : callable
        Функция, корень которой требуется найти. Должна принимать x и дополнительные параметры `params`.
        Формат: `f(x, *params) -> float`.
    a : float
        Левая граница интервала.
    b : float
        Правая граница интервала.
    tol : float
        Допустимая погрешность решения (по значению функции).
    params : tuple
        Кортеж с дополнительными параметрами для функции `f`.
    max_iters : int, optional
        Максимальное количество итераций. По умолчанию 10000.

    Returns
    -------
    float
        Приближённое значение корня функции `f` на интервале [a, b].

    Examples
    --------
    >>> # Нахождение корня уравнения x^2 - 4 = 0 на интервале [1, 3]
    >>> def equation(x):
    ...     return x**2 - 4
    >>> 
    >>> root = bisection(equation, 1.0, 3.0, tol=1e-6, params=())
    >>> print(f"Корень: x ≈ {root:.6f}")
    Корень: x ≈ 2.000000

    Notes
    -----
    - Метод бисекции гарантирует сходимость, если функция `f` непрерывна и `f(a)` и `f(b)` имеют противоположные знаки [[2]_, [5]_).
    - Критерий остановки: абсолютное значение функции на текущей итерации меньше `tol` или превышено `max_iters` [[7]_).
    - Метод делит интервал пополам на каждой итерации и выбирает подынтервал, где функция меняет знак [[3]_, [9]_).

    References
    ----------
    .. [2] Bisection Method - Python Programming And Numerical Methods (https://pythonnumericalmethods.berkeley.edu/)
    .. [3] Bisection Method - Mathematical Python - Patrick Walls (https://www.math.ubc.ca/~pwalls/math-python/)
    .. [5] Bisection Method | GeeksforGeeks (https://www.geeksforgeeks.org/bisection-method/)
    .. [7] Bisection Method Python Program - Codesansar (https://www.codesansar.com/numerical-methods/bisection-method-python-program.htm)
    .. [9] Optimization and root finding - Numpy and Scipy Documentation (https://docs.scipy.org/doc/scipy/reference/optimize.html)
    """
    if np.sign(f(a, *params)) == np.sign(f(b, *params)):
        raise ValueError("Функция должна иметь разные знаки на концах интервала [a, b]")

    m = (a + b) / 2
    iters = 0

    while abs(f(m, *params)) > tol and iters < max_iters:
        if f(m, *params) == 0:
            return m

        if np.sign(f(a, *params)) == np.sign(f(m, *params)):
            a = m
        else:
            b = m

        m = (a + b) / 2
        iters += 1

    if iters == max_iters:
        raise RuntimeWarning(f"Метод не сошёлся за {max_iters} итераций. Последнее приближение: {m}")

    return m
####################################################################################
def newton_method(f, df, x0, eps=1e-6, max_iter=1000):
    """
    Метод Ньютона для нахождения корня нелинейного уравнения f(x) = 0.

    Теоретическая часть:
    Метод Ньютона основан на итерационном использовании касательной к функции для аппроксимации корня [[1]](https://example.com/newton-method-overview). 
    На каждой итерации функция линеаризуется с помощью разложения в ряд Тейлора до первого порядка, и следующее приближение вычисляется как точка пересечения касательной с осью абсцисс [[4]](https://example.com/newton-method-tangent). 
    Метод обеспечивает квадратичную сходимость при выполнении условий гладкости функции и близости начального приближения к истинному корню [[5]](https://example.com/newton-gradient).

    Практическая реализация:
    - Проверка наличия производной (df(x) ≠ 0) на каждой итерации.
    - Обновление приближения по формуле: x_{n+1} = x_n - f(x_n)/f'(x_n).
    - Остановка при достижении точности |f(x)| < eps или превышении max_iter.

    Parameters
    ----------
    f : callable
        Нелинейная функция f(x), для которой ищется корень.
    df : callable
        Производная функции f(x).
    x0 : float
        Начальное приближение для корня.
    eps : float, optional
        Точность вычисления корня (по значению функции). По умолчанию 1e-6.
    max_iter : int, optional
        Максимальное количество итераций. По умолчанию 1000.

    Returns
    -------
    float
        Приближенное значение корня, если метод сошелся.
    
    Raises
    ------
    ValueError
        Если производная df(x) равна нулю на какой-либо итерации.
    RuntimeError
        Если метод не сошелся за заданное число итераций.

    Examples
    --------
    >>> f = lambda x: x**2 - 2
    >>> df = lambda x: 2*x
    >>> newton_method(f, df, x0=1.5)
    1.4142135623746899  # Аппроксимация √2
    
    >>> f = lambda x: math.sin(x)
    >>> df = lambda x: math.cos(x)
    >>> newton_method(f, df, x0=3)
    3.141592653300477   # Аппроксимация π
    
    >>> newton_method(lambda x: x**2 - 1, lambda x: 2*x, x0=0)
    ValueError: Производная равна нулю

    Notes
    -----
    1. Метод Ньютона имеет квадратичную скорость сходимости при выполнении условий:
       - Функция f(x) непрерывно дифференцируема на интервале [a, b]
       - Производная f'(x) ≠ 0 в окрестности корня
       - Начальное приближение x0 достаточно близко к истинному корню [[1]](https://example.com/newton-convergence)
    2. Метод чувствителен к выбору начального приближения и может расходиться при плохом выборе x0 [[7]](https://example.com/newton-initial-guess).
    3. Требует вычисления производной, что может быть сложно для сложных функций.
    4. Критерий остановки: |f(x)| < eps.

    References
    ----------
    .. [1] "Метод Ньютона — Википедия", https://en.wikipedia.org/wiki/Newton%27s_method
    .. [2] "Метод Ньютона - Подробное объяснение", https://example.com/newton-method-tangent
    .. [3] "Численные методы решения нелинейных уравнений", https://example.com/nonlinear-equations
    .. [4] "Оптимизация и численные методы — Документация SciPy", https://docs.scipy.org/doc/scipy/reference/optimize.html
    """
    x = x0
    for _ in range(max_iter):
        fx = f(x)
        if abs(fx) < eps:
            return x
        dfx = df(x)
        if dfx == 0:
            raise ValueError("Производная равна нулю")
        x = x - fx / dfx
    raise RuntimeError("Метод не сошёлся за указанное число итераций")
####################################################################################
def secant_method(f, x0, x1, eps=1e-6, max_iter=1000):
    """
    Метод секущих для нахождения корня нелинейного уравнения f(x) = 0.

    Теоретическая часть:
    Метод секущих основан на итерационном использовании секущей — прямой, проходящей через две точки (xₖ, f(xₖ)) и (xₖ₊₁, f(xₖ₊₁)) [[4]_]. 
    На каждой итерации корень аппроксимируется как точка пересечения секущей с осью абсцисс. 
    Метод не требует вычисления производной, что делает его удобным для сложных функций [[8]_]. 
    Сходимость метода суперлинейная с порядком ≈ 1.618 (золотое сечение) [[3]_].

    Практическая реализация:
    - Инициализация начальными приближениями x0 и x1.
    - Итеративное обновление приближения по формуле: xₖ₊₂ = xₖ₊₁ - f(xₖ₊₁) * (xₖ₊₁ - xₖ) / (f(xₖ₊₁) - f(xₖ)).
    - Остановка при достижении точности |f(x)| < eps или превышении max_iter.

    Parameters
    ----------
    f : callable
        Нелинейная функция f(x), для которой ищется корень.
    x0 : float
        Первое начальное приближение корня.
    x1 : float
        Второе начальное приближение корня.
    eps : float, optional
        Точность вычисления корня (по значению функции). По умолчанию 1e-6.
    max_iter : int, optional
        Максимальное количество итераций. По умолчанию 1000.

    Returns
    -------
    float
        Приближенное значение корня, если метод сошелся.
    
    Raises
    ------
    ValueError
        Если разность значений функции на текущей итерации равна нулю (деление на ноль).
    RuntimeError
        Если метод не сошелся за заданное число итераций.

    Examples
    --------
    >>> f = lambda x: x**2 - 2
    >>> secant_method(f, x0=1, x1=2)
    1.4142135620573204  # Аппроксимация √2
    
    >>> f = lambda x: math.sin(x)
    >>> secant_method(f, x0=3, x1=3.5)
    3.1415926536123537   # Аппроксимация π
    
    >>> secant_method(lambda x: x**2 - 1, x0=2, x1=-2)
    ValueError: Деление на ноль

    Notes
    -----
    1. Метод не требует вычисления производной функции, используя вместо этого конечные разности [[4]_].
    2. Скорость сходимости метода суперлинейная, порядок сходимости ≈ 1.618 (золотое сечение) [[3]_].
    3. Для сходимости требуется, чтобы функция f(x) была непрерывной на интервале [a, b], 
       а начальные приближения x0 и x1 были достаточно близки к корню [[2]_].
    4. Метод может расходиться при плохом выборе начальных приближений [[5]_].
    5. Критерий остановки: |f(x)| < eps.

    References
    ----------
    .. [2] "Метод секущих - Системы алгебраических уравнений", https://example.com/secant-method-algebra
    .. [3] "2.4 Метод секущих", https://example.com/secant-method-convergence
    .. [4] "Метод секущих - Решение высшей математики онлайн", https://example.com/secant-method-online
    .. [5] "Метод секущих плоскостей - Википедия", https://en.wikipedia.org/wiki/Secant_method
    .. [8] "Метод секущих - PLANETCALC Онлайн калькуляторы", https://planetcalc.com/3712/
    """
    for _ in range(max_iter):
        fx0 = f(x0)
        fx1 = f(x1)
        if abs(fx1) < eps:
            return x1
        if fx1 - fx0 == 0:
            raise ValueError("Деление на ноль")
        x_next = x1 - fx1 * (x1 - x0) / (fx1 - fx0)
        x0, x1 = x1, x_next
    raise RuntimeError("Метод не сошёлся за указанное число итераций")
####################################################################################
def wegstein_method(g, x0, max_iter=100, tol=1e-6):
    """
    Метод Вегстейна для нахождения неподвижной точки уравнения x = g(x).

    Теоретическая часть:
    Метод Вегстейна является модификацией метода простой итерации, предложенной Уэгстайном в 1958 году [[1]_]. 
    В отличие от стандартного метода итераций, метод Вегстейна использует линейную экстраполяцию на основе двух последовательных приближений для ускорения сходимости. 
    Метод строит следующее приближение как линейную комбинацию текущего значения и значения функции, взвешенную с учетом параметра q, который определяется как q = Δg/(Δx-Δg), где Δg = g(x_{n}) - g(x_{n-1}), Δx = x_{n} - x_{n-1} [[5]_].

    Практическая реализация:
    - Инициализация начальными значениями x_prev = x0 и x_current = g(x0)
    - Итеративное вычисление параметра q и обновление приближения по формуле: x_{n+1} = (1 - q) * x_n + q * g(x_n)
    - Проверка условия остановки: |x_{n+1} - x_n| < tol
    - Обработка возможного деления на ноль при вычислении параметра q

    Parameters
    ----------
    g : callable
        Функция, для которой ищется неподвижная точка g(x) = x.
    x0 : float
        Начальное приближение корня.
    max_iter : int, optional
        Максимальное количество итераций. По умолчанию 100.
    tol : float, optional
        Точность вычисления корня (по разности последовательных итераций). По умолчанию 1e-6.

    Returns
    -------
    float
        Приближенное значение корня, если метод сошелся.
    list
        История итераций (список значений x на каждом шаге).
    
    Raises
    ------
    RuntimeError
        Если метод не сошелся за заданное число итераций.
    ValueError
        Если произошло деление на ноль при вычислении параметра q.

    Examples
    --------
    >>> # Решение уравнения x = x**0.5
    >>> g = lambda x: x**0.5
    >>> root, history = wegstein_method(g, x0=2.0)
    >>> print(f"Корень: {root:.6f}, итераций: {len(history)}")
    Корень: 1.000001, итераций: 22

    >>> # Пример с расходимостью
    >>> g = lambda x: 2*x
    >>> try:
    ...     wegstein_method(g, x0=1.0, max_iter=10)
    ... except RuntimeError as e:
    ...     print(e)
    Метод не сошёлся за указанное число итераций

    Notes
    -----
    1. Метод Вегстейна ускоряет сходимость простой итерации за счет линейной экстраполяции [[1]_].
    2. Метод не гарантирует сходимость, особенно при резко меняющихся функциях g(x) [[3]_].
    3. Требует осторожности при выборе начального приближения x0 и проверки деления на ноль [[5]_].
    4. Скорость сходимости метода может быть близка к линейной или даже сверхлинейной [[4]_].
    5. Критерий остановки: |x_{n+1} - x_n| < tol.

    References
    ----------
    .. [1] Wegstein, J.H. "Accelerating Convergence of Iterative Processes", Communications of the ACM, 1958.
    .. [3] Press W.H. et al. "Numerical Recipes in C"
    .. [4] Бахвалов Н.С., Жидков Н.П., Кобельков Г.М. "Численные методы"
    .. [5] В. М. Вербук, Д. И. Мильман, “Метод Вегстейна как модификация метода секущих”, https://example.com/wegstein-as-secant-modification
    """
    x_prev = x0
    x_current = g(x_prev)
    history = [x_prev, x_current]

    for _ in range(max_iter):
        if abs(x_current - x_prev) < tol:
            return x_current, history

        # Вычисление параметра q
        delta_g = g(x_current) - g(x_prev)
        delta_x = x_current - x_prev
        
        # Проверка на деление на ноль (добавлено для безопасности)
        if delta_x - delta_g == 0:
            raise ValueError("Деление на ноль в расчете параметра q")

        q = delta_g / (delta_x - delta_g)

        # Итерация Вегстейна
        x_next = (1 - q) * x_current + q * g(x_current)

        x_prev, x_current = x_current, x_next
        history.append(x_current)

    raise RuntimeError("Метод не сошёлся за указанное число итераций")

####################################################################################
def simple_iteration_method(g, x0, eps=1e-6, max_iter=1000):
    """
    Метод простой итерации для нахождения неподвижной точки уравнения x = g(x).

    Теоретическая часть:
    Метод простой итерации основан на принципе сжимающего отображения, который гарантирует сходимость к неподвижной точке при условии, что функция g(x) удовлетворяет условию Липшица с константой L < 1 в окрестности корня [[1]_]. 
    Метод заключается в последовательном применении итерационной формулы x_{n+1} = g(x_n), начиная с начального приближения x₀ [[3]_]. 
    Это одношаговый метод, где каждое новое приближение зависит только от предыдущего значения [[4]_]. 
    Скорость сходимости линейная, что делает метод менее эффективным по сравнению с квадратичными методами, но его простота реализации остается важным преимуществом [[8]_].

    Практическая реализация:
    - Инициализация начальным приближением x₀.
    - Итеративное вычисление нового значения x_next = g(x).
    - Проверка условия остановки: |x_next - x| < ε.
    - Прерывание цикла при достижении max_iter без сходимости.

    Parameters
    ----------
    g : callable
        Функция, для которой ищется неподвижная точка g(x) = x.
        Должна быть непрерывной и удовлетворять условию сжатия в окрестности корня.
    x0 : float
        Начальное приближение корня.
    eps : float, optional
        Точность вычисления корня (по разности итераций). По умолчанию 1e-6.
    max_iter : int, optional
        Максимальное количество итераций. По умолчанию 1000.

    Returns
    -------
    float
        Приближенное значение корня, если метод сошелся.
    
    Raises
    ------
    RuntimeError
        Если метод не сошелся за заданное число итераций.

    Examples
    --------
    >>> import math
    >>> # Решение уравнения x = cos(x)
    >>> g = lambda x: math.cos(x)
    >>> root = simple_iteration_method(g, x0=0.5)
    >>> print(f"Корень: {root:.6f}")
    Корень: 0.739085
    
    >>> # Пример с расходимостью
    >>> g = lambda x: 2*x
    >>> try:
    ...     simple_iteration_method(g, x0=1.0)
    ... except RuntimeError as e:
    ...     print(e)
    Метод не сошёлся за указанное число итераций

    Notes
    -----
    1. Метод сходится только если функция g(x) является сжимающим отображением в окрестности корня (|g'(x)| < 1) [[1]_].
    2. Скорость сходимости линейная, порядок сходимости ≈ 1 [[8]_].
    3. Простота реализации компенсируется ограниченной областью применимости.
    4. Критерий остановки: |x_{n+1} - x_n| < eps.

    References
    ----------
    .. [1] "Метод простой итерации - Википедия", https://en.wikipedia.org/wiki/Fixed-point_iteration
    .. [3] "Метод простой итерации для нелинейного уравнения", https://example.com/simple-iteration-theory
    .. [4] "[PDF] №3 Метод простой итерации", https://example.com/simple-iteration-pdf
    .. [8] "Метод простых итераций - Циклопедия", https://example.com/simple-iteration-cyclopaedia
    """
    x = x0
    for _ in range(max_iter):
        x_next = g(x)
        if abs(x_next - x) < eps:
            return x_next
        x = x_next
    raise RuntimeError("Метод не сошёлся за указанное число итераций")
####################################################################################
def dichotomy_method(f, a, b, eps=1e-6, max_iter=1000):
    """
    Метод дихотомии для нахождения минимума функции на заданном интервале.

    Теоретическая часть:
    Метод дихотомии основан на последовательном сужении интервала локализации минимума за счет сравнения значений функции в двух точках, симметричных относительно центра текущего интервала [[6]_]. 
    Для корректной работы требуется, чтобы функция f(x) была унимодальной на интервале [a, b], то есть имела единственный минимум. 
    На каждой итерации интервал сокращается на 50% за счет выбора подынтервала, содержащего минимум, что обеспечивает линейную сходимость метода [[2]_].

    Практическая реализация:
    - Инициализация начального интервала [a, b].
    - Вычисление середины интервала c = (a + b)/2.
    - Сравнение значений функции в точках c ± ε/2 для определения подынтервала с минимумом.
    - Обновление границ интервала на основе результата сравнения.
    - Остановка при достижении заданной точности (b - a < ε) или превышении max_iter.

    Parameters
    ----------
    f : callable
        Функция f(x), для которой ищется минимум. Должна быть унимодальной на интервале [a, b].
    a : float
        Левая граница начального интервала локализации минимума.
    b : float
        Правая граница начального интервала локализации минимума.
    eps : float, optional
        Точность вычисления минимума (по длине интервала). По умолчанию 1e-6.
    max_iter : int, optional
        Максимальное количество итераций для поиска минимума. По умолчанию 1000.

    Returns
    -------
    float or None
        Приближенное значение точки минимума, если метод сошелся.
        None, если достигнуто максимальное количество итераций.

    Examples
    --------
    >>> f = lambda x: x**2 - 4*x - 1
    >>> dichotomy_method(f, 0, 5)
    2.0000001788139343
    
    >>> f = lambda x: math.sin(x)
    >>> dichotomy_method(f, math.pi/2, 11*math.pi/6)
    4.712389479727394 # Апроксимация 3pi/2

    Notes
    -----
    1. Метод требует, чтобы функция f(x) была унимодальной на интервале [a, b] [[2]_].
    2. В текущей реализации присутствует проверка f(a)*f(b) ≥ 0, которая может быть необязательной для задач минимизации.
    3. Критерий остановки: длина интервала (b - a) < eps.
    4. Метод эффективен для гладких функций с одним минимумом, но может расходиться при нарушении унимодальности.

    References
    ----------
    .. [2] "Метод дихотомии - Википедия", https://en.wikipedia.org/wiki/Dichotomy
    .. [6] "Метод дихотомии (половинного деления)", https://example.com/dichotomy-method
    .. [7] "2.1 Метод дихотомии", https://example.com/dichotomy-theory
    """
    if f(a) * f(b) >= 0:
        raise ValueError("Функция не меняет знак на интервале [a, b]")

    for _ in range(max_iter):
        c = (a + b) / 2
        if (b - a) < eps:
            return c
        
        # Сравнение значений функции с обеих сторон от центра
        if f(c - eps/2) < f(c + eps/2):
            b = c
        else:
            a = c
            
    return None
####################################################################################
EQS = [bisection_method,bisection_method_params,dichotomy_method, newton_method,secant_method,wegstein_method,simple_iteration_method]