Исходный код miptlabs.approximators.base

"""
Модуль содержаший базовые апроксиматоры
"""

from warnings import warn

import numpy as np
import scipy.fftpack
import scipy.optimize

from ..utils import round_to_n, format_monoid


[документация]class Approximator: """ Базовый класс апроксиматор. Классы нужны, чтобы сохранять разные данные, во время аппроксимации. Например коэффициенты, ошибки. Возможно когда-нибудь будет сделан класс для результата аппроксимации Для некоторых позволяют генерировать формулу в латехе """ def __init__(self, points=100, left_offset=5, right_offset=5): """ :param points: количество точек, которые будут на выходе :param left_offset: отступ от левой гриницы диапозона :param right_offset: отступ от правой гриницы диапозона """ self.left_offset = left_offset / 100 self.right_offset = right_offset / 100 self.points = points self.meta = [] # Данные self._x = np.array([]) self._y = np.array([]) self._xerr = np.array([]) self._yerr = np.array([]) # Коэффициенты апроксимации self.koefs = np.array([]) # Стандартное отклонение параметров self.sigmas = np.array([]) # Наиболее правдоподобная функция self._function = None def __init_subclass__(cls, **kwargs): """ Костыльное добавление документации к классам наследникам """ super().__init_subclass__(**kwargs) if not cls.__init__.__doc__: return if not cls.__doc__: cls.__doc__ = cls.__init__.__doc__ return # для совместимости со старыми питонами def remove_pref(s: str, pref: str): if s.startswith(pref): return s[len(pref):] return s cls.__doc__ = '{}\n{}'.format( cls.__doc__, '\n'.join( [ remove_pref(remove_pref(line, '\t'), ' ' * 4) for line in cls.__init__.__doc__.split('\n') ] ) ) def _gen_x_axis_with_offset(self, start, end): """ Генерирует набор точек по оси абсцисс в заданом диапозоне с учетом отступов. Функция нужна для внутрених нужд :param start: начало диапозона :param end: конец диапозона :return: """ delta = end - start return np.linspace(start - delta * self.left_offset, end + delta * self.right_offset, self.points)
[документация] def gen_x_axis(self, start, end): """ Генерирует набор точек по оси абсцисс в заданом диапозоне :param start: начало диапозона :param end: конец диапозона :return: """ return np.linspace(start, end, self.points)
def _prepare_before_approximation(self, x, y, xerr, yerr): if not isinstance(xerr, np.ndarray): xerr = np.ones_like(y) * xerr if not isinstance(yerr, np.ndarray): yerr = np.ones_like(y) * yerr self._x = np.array(x) self._y = np.array(y) self._xerr = xerr self._yerr = yerr return self._x, self._y, self._xerr, self._yerr
[документация] def approximate(self, x, y, xerr=0, yerr=0): """ Функция апроксимации :param x: набор параметров оси x :param y: набор параметров оси y :param xerr: погрешность параметров оси x :param yerr: погрешность параметров оси y :return: набор точек на кривой апроксимации """ self._x = np.array(x) self._y = np.array(y) self._xerr = xerr self._yerr = yerr return x, y
[документация] def get_function(self): """Возвращает полученную после аппроксимации функцию""" return self._function
[документация] def label(self, xvar='x', yvar='y'): """ Генерирует формулу для латеха :param xvar: буква перемонной по оси x :param yvar: буква перемонной по оси y :return: сгенерированная формулу """ return f'[{self.__class__.__name__}] function with params: {self.meta}'
[документация] def calc_hi_square(self): """ Вычисляет chi^2 """ if not self._yerr.all(): return None return np.sum(np.square((self._y - self.get_function()(self._x)) / self._yerr))
[документация] def calc_quality_of_approximation(self): """ Вычисляет chi^2 / (n - p) - n - количество точек в данных - p - количество степеней свободы (кол-во параметров в функции, которой аппроксимируем) """ if not self._yerr.all(): return None return self.calc_hi_square() / (len(self._x) - len(self.koefs))
[документация] def is_approximation_good(self): """ Делает оценочное суждение по поводу качества измерений quality_of_approximation = calc_quality_of_approximation = chi^2 / (n - p) - 0 < quality_of_approximation <= 0.5: Качество ваших измерений составляется quality_of_approximation <= 0.5. Это слишком мало, скорее всего это свидетельствуют о завышенных погрешностях - 0.5 < quality_of_approximation < 2: Качество ваших измерений составляется quality_of_approximation ~ 1. Это хороший результат. Ваша теоретическая модель хорошо сходится с экспериментом. - 2 <= quality_of_approximation: Качество ваших измерений составляется quality_of_approximation >= 2. Это слишком много. Это свидетельствуют либо о плохом соответствии теории и результатов измерений, либо о заниженных погрешностях. """ if not self._yerr.all(): return 'У ваших измерений по вертикальной оси нет погрешности, ' \ 'поэтому невозможно пользоваться методом хи-квадрат' if len(self._x) - len(self.koefs) <= 0: return 'Слишком мало точек для аппроксимации, воспользуйтесь интерполяцией' quality_of_approximation = self.calc_quality_of_approximation() if 0 < quality_of_approximation <= 0.5: return f'Качество ваших измерений составляется {round(quality_of_approximation, 2)} <= 0.5.\n' \ f'Это слишком мало, скорее всего это свидетельствуют о завышенных погрешностях.' if 0.5 < quality_of_approximation < 2: return f'Качество ваших измерений составляется {round(quality_of_approximation, 2)} ~ 1.\n' \ f'Это хороший результат. Ваша теоретическая модель хорошо сходится с экспериментом.' if 2 <= quality_of_approximation: return f'Качество ваших измерений составляется {round(quality_of_approximation, 2)} >= 2.\n' \ f'Это слишком много. Это свидетельствуют либо о плохом соответствии \n' \ f'теории и результатов измерений, либо о заниженных погрешностях.'
[документация]class MultiLinearMixin: """ Добавляет возможность получить функции, которая считает значения между двумя точками на прямой в результирующей аппроксимации Полезно для аппроксиматоров, которые только двигают точки, например Lowess """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._res_x = None self._res_y = None
[документация] def get_function(self): def scalar_func(x): # При выходе за область определения возбуждаем исключение if x < np.min(self._x) or x > np.max(self._x): raise AttributeError( f'Значение x={x} выходит за область определения [{np.min(self._x)}, {np.max(self._x)}]') # Если есть значение в точке idx = np.where(np.isclose(self._x, x))[0] if len(idx) != 0: return self._res_y[idx[0]] # Ищем значение на прямой между двумя ближайшими по оси x точками upper_bound = np.where(self._x > x)[0][0] # lower_bound = np.where(self._x < x)[0][-1] lower_bound = upper_bound - 1 x1 = self._res_x[lower_bound] x2 = self._res_x[upper_bound] y1 = self._res_y[lower_bound] y2 = self._res_y[upper_bound] k = (y2 - y1) / (x2 - x1) return k * x + y1 - k * x1 def vector_func(x): # При выходе за область определения возбуждаем исключение if np.any(x < np.min(self._x)) or np.any(x > np.max(self._x)): raise AttributeError( f'Одна из координат x={x} выходит за область определения [{np.min(self._x)}, {np.max(self._x)}]') return np.array([scalar_func(_x_comp) for _x_comp in x]) def inner(x): if isinstance(x, np.ndarray): return vector_func(x) else: return scalar_func(x) return inner
[документация]class Polynomial(Approximator): """ Аппроксимация с помощью полинома. Использует numpy.polyfit """ def __init__(self, deg=1, points=100, left_offset=5, right_offset=5): """ :param deg: степень апроскимируещего полинома :param points: количество точек, которые будут на выходе :param left_offset: отступ от левой гриницы диапозона :param right_offset: отступ от правой гриницы диапозона """ super(Polynomial, self).__init__(points, left_offset, right_offset) self.deg = deg
[документация] def approximate(self, x, y, xerr=0, yerr=0): x, y, xerr, yerr = self._prepare_before_approximation(x, y, xerr, yerr) result = np.polyfit(x, y, w=1 / yerr, deg=self.deg, cov=True) popt = result[0] pcov = result[-1] self.meta = self.meta = { 'popt': popt, 'pcov': pcov } self.koefs = popt self.sigmas = np.sqrt(np.diag(pcov)) self._function = np.poly1d(popt) poly = np.poly1d(popt) xs = self._gen_x_axis_with_offset(min(x), max(x)) return xs, poly(xs)
[документация] def label(self, xvar='x', yvar='y'): # если степень равна 0, то возвращаем эту константу if self.deg == 0: return f'${yvar} = {format_monoid(self.koefs[0], True)}$' # списисок моноидов monoids = [] # форматируем коэффициент при каждой степени, кроме первой и нулевой for i in range(self.deg - 1): monoid = f'{format_monoid(self.koefs[i])}{xvar}^{{{self.deg - i}}}' monoids.append(monoid) # форматируем коэффициент при первой степени monoids.append(f'{format_monoid(self.koefs[self.deg - 1])}{xvar}') # форматируем коэффициент при нулевой степени monoids.append(f'{format_monoid(self.koefs[self.deg])}') # объединяем в один полином res = ''.join(monoids) # убираем плюс при максимальной степени # FIXME неоптимизированный костыль с копирование строк if self.koefs[0] >= 0: res = res[1:] return f"${yvar} = {res}$"
[документация]class Functional(Approximator): """ Аппроксимация с помощью пользовательской функции. Использует scipy.optimize.curve_fit Функция должна иметь следующую сигнатуру: .. code:: python def function_for_fit(x, param1, param2, ..., paramn): ... x - переменная param1, param2, ..., paramn - параметры, которые будут искаться Например: .. code:: python def exp(x, a, b, c): return a * np.exp(b * x) + c У этой функции будут определяться параметры a, b, c """ def __init__(self, function, points=100, left_offset=5, right_offset=5): """ :param function: функция для аппроксимации :param points: количество точек, которые будут на выходе :param left_offset: отступ от левой границы диапазона :param right_offset: отступ от правой границы диапазона """ super(Functional, self).__init__(points, left_offset, right_offset) self._function_for_fit = function
[документация] def approximate(self, x, y, xerr=None, yerr=None): x, y, xerr, yerr = self._prepare_before_approximation(x, y, xerr, yerr) # пытаемся аппроксимировать. если у scipy не получается, то оно выбрасывает исключение RuntimeError try: popt, pcov = scipy.optimize.curve_fit( f=self._function_for_fit, xdata=x, ydata=y, sigma=yerr) perr = np.sqrt(np.diag(pcov)) self.meta = { 'popt': popt, 'pcov': pcov } self.koefs = popt self.sigmas = perr def fff(*params): def inner(xx): return self._function_for_fit(xx, *params) return inner self._function = fff(*self.koefs) xs = self._gen_x_axis_with_offset(min(x), max(x)) ys = self._function_for_fit(xs, *self.koefs) return xs, ys except RuntimeError: # Если вызывается исключение, то возвращаем исходные данные warn(f"Точки плохо подходят под апроксимацию выбранной функцией {self._function.__name__}") return x, y
[документация] def label(self, xvar='x', yvar='y'): try: return f'function with params: {[round_to_n(param, 3) for param in self.koefs]}' except IndexError: return f'Функция {self._function.__name__}, которая плохо подходит'