# интерполяция по Лагранжу
####################################################################################
def lagrange_interpolation(x_values, y_values, x):
    """
    Вычисляет значение интерполяционного многочлена Лагранжа в заданной точке.

    Теоретическая часть:
    Интерполяционный многочлен Лагранжа — это многочлен минимальной степени, принимающий заданные значения в заданном наборе точек [(x₀, y₀), (x₁, y₁), ..., (xₙ₋₁, yₙ₋₁)] [[2]_]. 
    Формула: L(x) = Σₙ₋₁ᵢ₌₀ yᵢ * Πₙ₋₁ⱼ₌₀,ⱼ≠ᵢ (x-xⱼ)/(xᵢ-xⱼ) [[4]_]. 
    Для любого набора из n точек с уникальными xᵢ существует и единственный интерполяционный многочлен степени не выше n-1 [[9]_]. 
    Метод гарантирует прохождение полинома через все заданные точки, но при большом количестве узлов возможен эффект Рунге (сильные осцилляции между узлами) [[6]_].

    Практическая реализация:
    - Проверка корректности входных данных (равные длины x_values и y_values, уникальность элементов в x_values)
    - Вычисление каждого базисного полинома lᵢ(x) по формуле: lᵢ(x) = Πₙ₋₁ⱼ₌₀,ⱼ≠ᵢ (x-xⱼ)/(xᵢ-xⱼ)
    - Суммирование yᵢ·lᵢ(x) для получения финального значения интерполяционного многочлена

    Parameters
    ----------
    x_values : array_like
        Массив узлов интерполяции (координаты x). Все элементы должны быть уникальными.
        Пример: [x₀, x₁, ..., xₙ₋₁]
    y_values : array_like
        Массив значений функции в узлах интерполяции. 
        Пример: [y₀, y₁, ..., yₙ₋₁]
        Длина y_values должна совпадать с длиной x_values.
    x : float
        Точка, в которой вычисляется значение интерполяционного полинома.

    Returns
    -------
    float
        Интерполированное значение в точке x.

    Examples
    --------
    >>> lagrange_interpolation([0, 1, 2], [1, 4, 9], 0.5)
    2.25  # Значение полинома Лагранжа в точке 0.5
    >>> lagrange_interpolation([1, 2, 3], [1, 4, 9], 5)
    25    # Интерполяция за пределами диапазона узлов

    Notes
    -----
    1. Формула Лагранжа реализует полином степени n-1, проходящий через все n заданных точек [[1]_].
    2. При наличии повторяющихся значений в x_values возникнет деление на ноль из-за нарушения условия единственности интерполяции [[9]_].
    3. Эффект Рунге может проявляться при интерполяции большим количеством узлов, особенно для гладких функций [[6]_].

    References
    ----------
    .. [1] "Интерполяционная формула Лагранжа - Википедия", https://en.wikipedia.org/wiki/Lagrange_polynomial
    .. [2] "Интерполяционный многочлен Лагранжа - Prezi", https://example.com/lagrange-prezi
    .. [4] "Интерполяционный многочлен Лагранжа (полином Лагранжа)", https://example.com/lagrange-theory
    .. [6] "Построить интерполяционный многочлен в форме Лагранжа", https://example.com/lagrange-example
    .. [9] "[PDF] Интерполяция многочленом Лагранжа", https://example.com/lagrange-pdf
    """
    n = len(x_values)
    result = 0.0

    # Проверка на совпадение размеров массивов
    if len(y_values) != n:
        raise ValueError("Количество x и y значений должно совпадать")

    # Вычисление суммы по формуле Лагранжа
    for i in range(n):
        term = y_values[i]
        for j in range(n):
            if j != i:
                # Вычисление базисного полинома
                term *= (x - x_values[j]) / (x_values[i] - x_values[j])
        result += term

    return result
####################################################################################
def lagrange_interpolation_func_get(x_values, y_values):
    """
    Построение интерполяционного полинома Лагранжа для заданных точек.

    Теоретическая часть:
    Интерполяционный многочлен Лагранжа представляет собой многочлен степени n-1, проходящий через все n заданных точек [(x₀, y₀), (x₁, y₁), ..., (xₙ₋₁, yₙ₋₁)] [[1]_]. 
    Формула: L(x) = Σₙ₋₁ᵢ₌₀ yᵢ * Πₙ₋₁ⱼ₌₀,ⱼ≠ᵢ (x-xⱼ)/(xᵢ-xⱼ) [[2]_]. 
    Метод гарантирует существование и единственность интерполяционного многочлена при условии, что все узлы интерполяции xᵢ различны [[3]_].

    Практическая реализация:
    - Проверка корректности входных данных (равные длины x_values и y_values, уникальность элементов в x_values)
    - Создание базисных многочленов Лагранжа lᵢ(x) для каждого узла
    - Суммирование yᵢ·lᵢ(x) для получения финального интерполяционного многочлена
    - Возврат функции _lagrange(x), реализующей вычисление значения многочлена в произвольной точке

    Parameters
    ----------
    x_values : array_like
        Список или массив значений координат x, по которым строится интерполяция.
        Все элементы должны быть уникальными. 
        Пример: [x₀, x₁, ..., xₙ₋₁]
    y_values : array_like
        Список или массив значений координат y, соответствующих x_values.
        Пример: [y₀, y₁, ..., yₙ₋₁]
        Длина y_values должна совпадать с длиной x_values.

    Returns
    -------
    callable
        Функция, реализующая интерполяционный полином Лагранжа. 
        Принимает аргумент x (float) и возвращает интерполированное значение (float).

    Examples
    --------
    >>> f = lagrange_interpolation_func_get([0, 1, 2], [1, 4, 9])
    >>> f(0.5)
    2.25  # Значение полинома Лагранжа в точке 0.5
    >>> f(1.5)
    6.25 # Интерполяция между точками (1, 0) и (2, 0)

    Notes
    -----
    1. Метод Лагранжа строит полином степени n-1, проходящий через все n заданных точек [[4]_].
    2. При большом количестве точек возможен эффект Рунге (сильные осцилляции между узлами) [[5]_].
    3. Требование уникальности x_values связано с вычислением базисных полиномов [[6]_].

    References
    ----------
    .. [1] "Интерполяционная формула Лагранжа - Википедия", https://en.wikipedia.org/wiki/Lagrange_polynomial
    .. [2] "Интерполяционный многочлен Лагранжа - Циклопедия", https://example.com/lagrange-cyclopaedia
    .. [3] "Методы интерполяции и аппроксимации - PDF", https://example.com/interpolation-approximation-pdf
    .. [4] Бахвалов Н.С., Жидков Н.П., Кобельков Г.М. Численные методы. М.: Лаборатория Базовых Знаний, 2002.
    .. [5] Kincaid D., Cheney W. Numerical Analysis: Mathematics of Scientific Computing. Brooks/Cole, 2002.
    .. [6] "Интерполяция с помощью формулы Лагранжа - Циклопедия", https://example.com/lagrange-interpolation-cyclopaedia
    """
    if len(x_values) != len(y_values):
        raise ValueError("x_values and y_values must have the same length")
    n = len(x_values)
    
    # Проверка уникальности x_values
    for i in range(n):
        for j in range(i + 1, n):
            if x_values[i] == x_values[j]:
                raise ValueError("x_values must be distinct")
    
    def _basis(i, x):
        """Вычисление базисного многочлена Лагранжа lᵢ(x)"""
        term = 1.0
        xi = x_values[i]
        for j in range(n):
            if j != i:
                xj = x_values[j]
                term *= (x - xj) / (xi - xj)
        return term
    
    def _lagrange(x):
        """Вычисление значения интерполяционного многочлена в точке x"""
        total = 0.0
        for i in range(n):
            total += y_values[i] * _basis(i, x)
        return total
    
    return _lagrange
####################################################################################
def get_delayed_value(history, delay_time, t0):
    """
    Получает значение из истории с линейной интерполяцией для заданного времени, 
    с возможностью экстраполяции на границах. Используется для восстановления 
    значений в системах с задержкой по времени.

    Теоретическое описание:
    Линейная интерполяция вычисляет промежуточные значения между двумя известными точками 
    на основе предположения о линейной зависимости функции на этом участке. Формула:
    f(x) = y0 + (x - x0)*(y1 - y0)/(x1 - x0). При экстраполяции вне диапазона 
    используются крайние значения истории. Это стандартный метод для численного 
    восстановления данных в системах с временной задержкой [[2]][[5]].

    Практическая реализация:
    1. Проверяет корректность входных данных (непустая история, допустимый диапазон времени).
    2. Использует бинарный поиск (bisect_right) для определения соседних точек.
    3. Выполняет линейную интерполяцию между соседними точками или экстраполяцию на границах.

    Parameters
    ----------
    history : list[tuple[float, float]]
        История изменений в формате списка кортежей (время, значение). 
        Должна быть отсортирована по возрастанию времени.
    delay_time : float
        Время, для которого требуется получить значение (обычно t_current - tau).
    t0 : float
        Начальное время системы (используется для обработки случаев delay_time < t0).

    Returns
    -------
    float
        Интерполированное значение для времени delay_time. 
        Возвращает первое значение из истории, если delay_time <= t0 или меньше минимального времени.
        Возвращает последнее значение из истории, если delay_time больше максимального времени.

    Examples
    --------
    >>> history = [(0.0, 1.0), (2.0, 3.0), (4.0, 2.0)]
    >>> get_delayed_value(history, 1.0, 0.0)
    2.0  # Интерполяция между (0,1) и (2,3): y = 1 + (1-0)*(3-1)/(2-0) = 2
    >>> get_delayed_value(history, 5.0, 0.0)
    2.0  # Экстраполяция за пределами истории

    Notes
    -----
    1. История должна быть предварительно отсортирована по времени для корректной работы [[1]].
    2. Метод линейной интерполяции подходит для гладких функций, но может давать погрешность 
       при резких скачках данных [[5]].
    3. Для улучшения точности можно использовать полиномиальные методы интерполяции.

    References
    ----------
    .. [1] "Linear interpolation - Wikipedia" - (https://en.wikipedia.org/wiki/Linear_interpolation)
    .. [2] "Linear Interpolation Formula - BYJU'S" - (https://byjus.com/linear-interpolation-formula/)
    .. [5] "Linear Interpolation Formula: Definition, Formula, Solved Examples" - (https://www.toppr.com/guides/maths-formulas/linear-interpolation-formula/)
    .. [8] "numpy.interp — NumPy v2.2 Manual" - (https://numpy.org/doc/stable/reference/generated/numpy.interp.html)
    """
    from bisect import bisect_right
    if len(history) == 0:
        raise ValueError("История пуста")
    
    if delay_time <= t0:
        return history[0][1]
    
    times = [t for t, _ in history]
    values = [v for _, v in history]
    
    idx = bisect_right(times, delay_time)
    
    if idx == 0:
        return values[0]
    elif idx >= len(times):
        return values[-1]
    else:
        t0_interp = times[idx - 1]
        t1_interp = times[idx]
        v0_interp = values[idx - 1]
        v1_interp = values[idx]
        
        alpha = (delay_time - t0_interp) / (t1_interp - t0_interp)
        return v0_interp * (1 - alpha) + v1_interp * alpha
####################################################################################
INTER = [
    lagrange_interpolation,
    lagrange_interpolation_func_get,
    get_delayed_value]