Привет, Хабр! Сегодня миниатюрный туториал о том, как сделать разбор строки с математическим выражением и вычислить его используя нечеткие треугольные числа. При соответствующих изменениях кода туториал сгодится для работы и с другими «кастомными» переменными. Справка: нечеткие треугольные числа — частный случай нечетких чисел (нечетких переменных на числовой оси). Ознакомиться подробнее рекомендую здесь и здесь.

Требования:

  • Язык программирования python 3.x (приводимый в статье код проверялся на python 3.5)
  • библиотека sympy, можно установить через терминал (консоль):

    pip install sympy

Порядок решения задачи:

  1. Подключаем библиотеки

    from fractions import Fraction
    import re
    from typing import Iterable
    from random import random
    
    import sympy

    Подключение fractions необязательно, будем использовать Fraction для хранения вещественных чисел в виде дроби (с целью минимальной потери точности). Библиотеку re будем использовать для парсинга строки и автоматического формирования списка символьных переменных.

    Использование библиотеки typing необязательно, используем ее для явного указания типов параметров функций. Библиотека random будет использоваться для формирования тестовых значений нечетких переменных. sympy — отличная библиотека для символьных вычислений в Python, с ее помощью мы будем работать с самой строкой-выражением.
  2. Опишем класс нечетких треугольных чисел и операций над ними. В данном примере достаточно трех операций (сложение, вычитание и деление). Вводить операции будем с помощью перегрузки «магических» методов соответствующего класса:

    class FuzzyTriangular(object):
        """Описание класса FuzzyTriangular"""
        def __init__(self, floatdigit = None, ABC = None, CAB = None, CDD = None):
            super(FuzzyTriangular, self).__init__()
            if ABC or floatdigit:
                if isinstance(floatdigit, (int, float)):
                    self._a = Fraction(floatdigit) #левый "0"
                    self._b = Fraction(floatdigit) #центр ("1")
                    self._c = Fraction(floatdigit) #правый "0"
                elif isinstance(floatdigit, (tuple,list)):
                    if len(floatdigit) == 2: #симметричное отклонение от центра
                        self._a = Fraction(floatdigit[0] - abs(floatdigit[1])) #левый "0"
                        self._b = Fraction(floatdigit[0]) #центр ("1")
                        self._c = Fraction(floatdigit[0] + abs(floatdigit[1])) #правый "0"
                    else: #3 и более, воспринимаются первые 3
                        self._a = Fraction(floatdigit[0]) #левый "0"
                        self._b = Fraction(floatdigit[1]) #центр ("1")
                        self._c = Fraction(floatdigit[2]) #правый "0"
                else:
                    self._a = Fraction(ABC[0]) #левый "0"
                    self._b = Fraction(ABC[1]) #центр ("1")
                    self._c = Fraction(ABC[2]) #правый "0"
    
                self._center = self._b #центр
                self._alpha = self._b - self._a #отклонение от центра влево
                self._beta = self._c - self._b #отклонение от центра вправо
                self._d = (self._alpha + self._beta)/2
                self._delta = (self._beta - self._alpha)/2
            elif CAB:
                self._center = Fraction(CAB[0]) #центр
                self._alpha = Fraction(CAB[1]) #отклонение от центра влево
                self._beta = Fraction(CAB[2]) #отклонение от центра вправо
                self._d = (self._alpha + self._beta)/2
                self._delta = (self._beta - self._alpha)/2
    
                self._b = self._center #центр ("1")
                self._a = self._center - self._alpha #левый "0"
                self._c = self._center + self._beta #правый "0"
            elif CDD:
                self._center = Fraction(CDD[0]) #центр
                self._d = Fraction(CDD[1])
                self._delta = Fraction(CDD[2])
                self._alpha = self._d - self._delta #отклонение от центра влево
                self._beta = self._d + self._delta #отклонение от центра вправо
    
                self._b = self._center #центр ("1")
                self._a = self._center - self._alpha #левый "0"
                self._c = self._center + self._beta #правый "0"
            else:
                raise Exception("No input data to create class")
    
        def __repr__(self):
            return str((round(float(self._a), 12), round(float(self._b), 12),            round(float(self._c), 12)))
    
        def __CDD_add(self, other):
            center = self._center + other._center
            d = self._d + other._d
            delta = self._delta + other._delta
            return FuzzyTriangular(CDD = (center, d, delta))
    
        def __CDD_sub(self, other):
            center = self._center - other._center
            d = self._d + other._d
            delta = self._delta - other._delta
            return FuzzyTriangular(CDD = (center, d, delta))
    
        def __CDD_mul(self, other):
            center = self._center*other._center
            d = abs(self._center)*other._d + abs(other._center)*self._d
            delta = self._center*other._delta + other._center*self._delta
            return FuzzyTriangular(CDD = (center, d, delta))
    
        def __add__(self, other):
            if isinstance(other, FuzzyTriangular):
                return self.__CDD_add(other)
            else:
                return self.__CDD_add(FuzzyTriangular(other))
    
        def __sub__(self, other):
            if isinstance(other, FuzzyTriangular):
                return self.__CDD_sub(other)
            else:
                return self.__CDD_sub(FuzzyTriangular(other))
    
        def __mul__(self,other):
            if isinstance(other, FuzzyTriangular):
                return self.__CDD_mul(other)
            else:
                return self.__CDD_mul(FuzzyTriangular(other))
    
        def __pos__(self):
            return FuzzyTriangular(1)*self
    
        def __neg__(self):
            return FuzzyTriangular(-1)*self
    
        def __eq__(self, other):
            return (self._a == other._a) and (self._b == other._b) and         (self._c == other._c)
    

    Формы представления нечетких треугольных чисел могут быть разные, не будем углубляться. В представленном коде обратим внимание на методы __add__ (оператор сложения), __sub__ (оператор вычитания), __mul__ (оператор умножения). Если попытаться к нечеткому треугольному числу прибавить вещественное число, то оно будет преобразовано в нечеткое треугольное. Аналогичная ситуация с кортежем или списком из вещественных чисел — первые три числа будут восприниматься как нечеткое треугольное (и также преобразовываться в класс FuzzyTriangular). Метод __pos__ переопределяет унарный оператор "+". Метод __neg__ — унарный "-". Метод __eq__ переопределяет оператор "==". При желании можно дополнительно переопределить такие операции как:

    • деление
    • возведение в степень
    • модуль числа
    • сравнения (больше/меньше, больше либо равно/меньше либо равно)
    • скаляризация (приведение к int, float, complex числам, округление)
    • инверсия и др...

    Проверить адекватность введенных операций можно небольшим набором тестов, например таких:

    ZERO = FuzzyTriangular((0,0,0))
    ONE = FuzzyTriangular((1,1,1))
    A = FuzzyTriangular((0.3,0.5,0.9))
    B = FuzzyTriangular((0.2,0.4,0.67))
    C = FuzzyTriangular((0,0.33,0.72))
    
    print('ZERO = '+str(ZERO))
    print('ONE = '+str(ONE))
    print('A = '+str(A))
    print('B = '+str(B))
    print('C = '+str(C))
    
    #some tests
    print('\nСЛОЖЕНИЕ')
    print('A + B = ', A + B)
    print('A + B == B + A', A + B == B + A) #введение оператора сравнения
    print('A + C = ', A + C)
    print('A + C == C + A', A + C == C + A)
    print('B + C = ', B + C)
    print('B + C == C + B', B + C == C + B)
    print('A + B + C = ', A + B + C)
    print('(A + B) + C == A + (B + C) == (A + C) + B',     (A + B) + C == A + (B + C) == (A + C) + B)
    print('C + 1 = ', C + 1)
    print('1 + C = ', ONE + C)
    
    print('\nВЫЧИТАНИЕ')
    print('A - A =', A - A)
    print('A - A == 0', A - A == ZERO)
    print('A - B = ', A - B)
    print('B - A = ', B - A)
    #введение унарных операторов "-" и "+"
    print('A - B == -(B - A)', A - B == -(B - A))
    print('(A + B + C) - (A + B) = ', (A + B + C) - (A + B))
    #необходимость использования рациональных дробей
    print('(A + B + C) - (A + B) == C', (A + B + C) - (A + B) == C)
    print('1 - A = ', ONE - A)
    print('A - 1 = ', A - 1)
    print('1 - A == -(A - 1)', ONE - A == -(A - 1))
    
    print('\nУМНОЖЕНИЕ')
    print('A*B == B*A', A*B == B*A)
    print('-1*C =', -ONE*C)
    print('-1*C == -C', -ONE*C == -C)
    print('-1*C == C*-1', -ONE*C == C*-1)
    print('C*-1 = ', C*-1)
    print('C*-1 =', C*-1)
    print('-C*1 == -C', -C*1 == -C)
    print('-C*1 =', -C*1)
    print('-C =', -C)
    print('C*-1 == -C', C*-1 == -C)
    print('(A + B)*C == A*C + B*C', (A + B)*C == A*C + B*C)
    print('(A - B)*C == A*C - B*C', (A - B)*C == A*C - B*C)
    print('A*C = ', A*C)
    print('B*C = ', B*C)
    print('-B*C = ', -B*C)
    print('-B*C == B*-C', -B*C == B*-C)
    print('B*C == -B*-C', B*C == -B*-C)

    Эти проверочные операции сложения, деления и умножения задаются в коде и выполняются согласно переопределению «магических» методов. Нам бы хотелось иметь возможность осуществлять такие же операции с использованием символьных переменных в заранее неизвестных выражениях. Для этого требуется ввести несколько вспомогательных функций.
  3. Вводим вспомогательные функции:

    • def symbols_from_expr(expr_str: str, pattern=r"[A-Za-z]\d{,2}") -> tuple:
          """Возвращает все найденные символические переменные по заданному шаблону"""
          symbols_set = set(re.findall(pattern, expr_str))
          symbols_set = sorted(symbols_set)
          symbols_list = tuple(sympy.symbols(symbols_set))
          return symbols_list
      Эту функцию будем использовать для поиска символьных переменных в строке-выражении (шаблон по умолчанию — символ от A до Z или от a до z и целое число после него длиной до 2х знаков (или отсутствие числа).
    • def expr_subs(expr_str: str, symbols: Iterable, values: Iterable):
          """Возвращает результат подстановки значений values вместо символов symbols в выражение-строку expr_str"""
          expr = sympy.sympify(expr_str)
          func = sympy.lambdify(tuple(symbols), expr, 'sympy')
          return func(*values)

      Эта функция позволяет вычислить значение выражения-строки с подстановкой вместо символьных переменных переменных любого допустимого типа (если для него переопределены операции, содержащиеся в самом выражении-строке). Это возможно благодаря функции sympy.lambdify, которая преобразует выражение sympy в лямбда-функцию, воспринимающую «магические» методы. Важным условием для адекватной работы функции является правильный порядок элементов в symbols и values (соответствие символов и подставляемых значений).
    • Каждый раз создавать лямбда-функцию дорогое удовольствие. Если требуется множественное использование одного и того же выражения, то рекомендуется воспользоваться следующими двумя функциями:

      def lambda_func(expr_str: str, symbols: Iterable) -> callable:
          """Возвращает лямбда-функцию, полученную путем преобразования выражения-строки expr_str с символами symbols"""
          expr = sympy.sympify(expr_str)
          func = sympy.lambdify(tuple(symbols), expr, 'sympy')
          return func
      
      def func_subs(expr_func: callable, values: Iterable):
          """Возвращает результат вызова лямбда-функции expr_func с параметрами values"""
          return expr_func(*values)

      Первая возвращает самую лямбда-функцию, а вторая — позволяет вычислять результирующие значения с помощью подстановки списка значений. В очередной раз акцентируется внимание на то, что используемые значения вовсе не обязаны быть треугольными нечеткими числами.

  4. Читаем строку-формулу из файла

    with open('expr.txt', 'r') as file:
        expr_str = file.read()
        print('expr_str', expr_str)

    В качестве формулы-строки для файла expr.txt можно использовать что-то вроде этого:

    p36*q67*p57*p26*p25*p13*q12*q15 +
    + p36*q67*p47*p26*p24*p13*q12 +
    + p67*q57*p26*p25*q12*p15 +
    + q57*p47*p25*p24*q12*p15 +
    + p57*p25*p12*q15 +
    + p36*p67*p13 +
    + p67*p26*p12 +
    + p47*p24*p12 +
    + p57*p15 -
    - p57*p47*p24*p12*p15 -
    - p67*p47*p26*p24*p12 -
    - p67*p57*p26*p12*p15 +
    + p67*p57*p47*p26*p24*p12*p15 -
    - p36*p67*p26*p13*p12 -
    - p36*p67*p47*p24*p13*p12 -
    - p36*p67*p57*p13*p15 +
    + p36*p67*p57*p47*p24*p13*p12*p15 +
    + p36*p67*p47*p26*p24*p13*p12 +
    + p36*p67*p57*p26*p13*p12*p15 -
    - p36*p67*p57*p47*p26*p24*p13*p12*p15 -
    - p36*p67*p57*p25*p13*p12*q15 -
    - p67*p57*p26*p25*p12*q15 -
    - p57*p47*p25*p24*p12*q15 +
    + p67*p57*p47*p26*p25*p24*p12*q15 +
    + p36*p67*p57*p26*p25*p13*p12*q15 +
    + p36*p67*p57*p47*p25*p24*p13*p12*q15 -
    - p36*p67*p57*p47*p26*p25*p24*p13*p12*q15 -
    - p36*p67*q57*p47*q26*p25*p24*p13*q12*p15 -
    - p67*q57*p47*p26*p25*p24*q12*p15 -
    - p36*p67*q57*p26*p25*p13*q12*p15 -
    - p36*q67*q57*p47*p26*p25*p24*p13*q12*p15 -
    - p36*q67*p57*p47*p26*p24*p13*q12*p15 -
    - p36*q67*p57*p47*p26*p25*p24*p13*q12*q15
  5. Вытаскиваем символьные переменные из выражения-строки:

    symbols = symbols_from_expr(expr_str)
    print('AutoSymbols', symbols)
  6. Генерируем тестовые случайные треугольные числа:

    values = tuple([FuzzyTriangular(sorted([random(),random(),random()]))    for i in range(len(symbols))])

    Сортировка случайных значений требуется для соответствия очередности значений левого «0», центра и правого «0».
  7. Преобразуем строку-формулу в выражение:

    func = lambda_func(expr_str, symbols)
    print('func', '=', func)
  8. Вычисляем значение формулы с помощью лямбда-функции (используем func_subs и expr_subs для того, чтобы удостовериться в совпадении результатов):

    print('func_subs', '=', func_subs(func, values))
    print('expr_subs', '=', expr_subs(expr_str, symbols, values))

Пример вывода:

expr_str p36*q67*p57*p26*p25*p13*q12*q15 +
+ p36*q67*p47*p26*p24*p13*q12 +
+ p67*q57*p26*p25*q12*p15 +
+ q57*p47*p25*p24*q12*p15 +
+ p57*p25*p12*q15 +
+ p36*p67*p13 +
+ p67*p26*p12 +
+ p47*p24*p12 +
+ p57*p15 -
- p57*p47*p24*p12*p15 -
- p67*p47*p26*p24*p12 -
- p67*p57*p26*p12*p15 +
+ p67*p57*p47*p26*p24*p12*p15 -
- p36*p67*p26*p13*p12 -
- p36*p67*p47*p24*p13*p12 -
- p36*p67*p57*p13*p15 +
+ p36*p67*p57*p47*p24*p13*p12*p15 +
+ p36*p67*p47*p26*p24*p13*p12 +
+ p36*p67*p57*p26*p13*p12*p15 -
- p36*p67*p57*p47*p26*p24*p13*p12*p15 -
- p36*p67*p57*p25*p13*p12*q15 -
- p67*p57*p26*p25*p12*q15 -
- p57*p47*p25*p24*p12*q15 +
+ p67*p57*p47*p26*p25*p24*p12*q15 +
+ p36*p67*p57*p26*p25*p13*p12*q15 +
+ p36*p67*p57*p47*p25*p24*p13*p12*q15 -
- p36*p67*p57*p47*p26*p25*p24*p13*p12*q15 -
- p36*p67*q57*p47*q26*p25*p24*p13*q12*p15 -
- p67*q57*p47*p26*p25*p24*q12*p15 -
- p36*p67*q57*p26*p25*p13*q12*p15 -
- p36*q67*q57*p47*p26*p25*p24*p13*q12*p15 -
- p36*q67*p57*p47*p26*p24*p13*q12*p15 -
- p36*q67*p57*p47*p26*p25*p24*p13*q12*q15
AutoSymbols (p12, p13, p15, p24, p25, p26, p36, p47, p57, p67, q12, q15, q26, q57, q67)
func = <function <lambda> at 0x06129C00>
func_subs = (-0.391482058715, 0.812813114469, 2.409570627378)
expr_subs = (-0.391482058715, 0.812813114469, 2.409570627378)
[Finished in 1.5s]

Туториал окончен. Надеюсь, что вы нашли здесь для себя что-то полезное!

P.S.: основная «фишка» описанного подхода — это возможность выйти за рамки стандартных для python и sympy типов переменных и операций над ними. Объявив свой класс и перегрузив «магические» методы вы можете вычислять заранее неизвестные математические выражения с помощью sympy (создавая лямбда-функции, воспринимающие как стандартные так и пользовательские типы и операции).

Спасибо за внимание!

Комментарии (11)


  1. Sirion
    10.03.2019 00:52
    +4

    Пост можно было бы сделать намного лучше, если бы вначале вставить параграф-другой про нечёткие треугольные числа: что это такое и с чем их едят. Я, например, мехмат, но впервые о них слышу. Осмелюсь на основании этого предположить, что очень большой процент читателей столкнётся с аналогичной трудностью.


    1. KvanTTT
      10.03.2019 02:51

      Ну или ссылку хотя быть дать на описание. Я сразу подумал про фигурные числа, частным случаем которых являются треугольные. Но в топике, по-видимому, речь о другом.


      1. Palich239 Автор
        10.03.2019 09:43

        Я даже не подумал про схожесть с фигурными числами… Да, действительно, речь не о них. Добавил две ссылки: на описание того, что такое нечеткое число и на описание того, что такое нечеткое треугольное число.


    1. Palich239 Автор
      10.03.2019 09:50

      Спасибо! Добавил в начало статьи небольшой комментарий по поводу нечетких треугольных чисел. Пусть вас не смущает то, что операции над этими числами в статье реализованы несколько иначе, чем в приведенных ссылках — в мире пока нет абсолютно устоявшегося подхода (виной проблема постоянного «расплыва» нечеткости осуществлении вычислений). Просто цимус туториала в другом — на примере работы с нечеткими треугольными числами здесь показано как с помощью python + sympy вычислять символьные мат.выражения с нестандартными операциями. Вы можете описать собственный класс нестандартных мат.конструкций, переопределить операции над ними и воспользоваться sympy.lambdify для формирования лямбда-функции, воспринимающей вашу пользовательскую математику.


  1. Quarc
    10.03.2019 10:45

    Ого, это можно использовать для вычислений с чиcлами типа a±?a в виде (a-?a, a, a+?a).
    PS: хотя для этого частного случая удобнее определить числа как (a, ?a).


    1. Palich239 Автор
      10.03.2019 10:50

      Можно, только обратите внимание на сами операции сложения/вычитания/умножения — мнения об их реализации расходятся, исследования еще идут, вероятно, еще будут идти… Вы можете изменить код на собственную реализацию.


      1. Quarc
        10.03.2019 11:19
        +1

        Бегло просмотрел код и не вижу что не так с вашими операциями:
        (a, ?a) + (b, ?b) = (a + b, ?a + ?b);
        (a, ?a) - (b, ?b) = (a - b, ?a + ?b);
        (a, ?a) * (b, ?b) = (a * b, abs(b)*?a + abs(a)*?b);
        (a, ?a) / (b, ?b) = (a / b, (?a + abs(a)*?b / abs(b)) / abs(b));
        (a, ?a)n = (an, n * an-1 * ?a);

        Кроме отсутствующих последних двух операций, все соответствует данному частному случаю.


        1. Palich239 Автор
          10.03.2019 11:24

          Все дело в растущем «расплыве», но это отдельная тема для беседы. Спасибо за полезный комментарий! Соответствие есть. Кстати, это хорошая мысль добавить перевод пары чисел (a, ?a) в треугольное (a, b, c). Добавил в статью.


        1. Quarc
          10.03.2019 12:20

          Ошибка в 4-ой строке должно быть:
          (a, ?a) / (b, ?b) = (a * b, abs(b)*?a + abs(a)*?b);


      1. iShrimp
        10.03.2019 18:24
        +1

        Сложение, вычитание, умножение на константу — операции линейные, они сохраняют симметричность, а вот умножение и деление — нелинейные операции, они приводят к разной длине левого и правого «хвостов».
        Конкретная реализация зависит от задачи, точнее от того, что означают для нас нечёткие числа. Если это — некая величина и её погрешность, типа a±?a, где ?a невелико, то при перемножении можно пользоваться приближенной формулой типа (a, ?a) * (b, ?b) = (a * b, abs(b)*?a + abs(a)*?b).
        Например, вычисление (2, 0.1) * (3, 0.1) даст (6, 0.5).
        Если «нечёткое число» — это заранее неизвестная величина, у которой известны «типичное» значение и интервал, за пределы которого она никогда не выходит, то оно будет представлено тремя параметрами (минимум, мода, максимум), которые могут принимать любые значения, лишь бы их последовательность была неубывающей. В этом случае для границ интервала придётся проводить точные вычисления, в т.ч. отслеживать переходы интервала через 0.
        Для того же примера из двух симметричных чисел (1.9, 2, 2.1) * (2.9, 3, 3.1) точный результат равен (5.51, 6, 6.51) — он оказался несимметричным.
        Ну а если нам важно количественно знать, как часто встречаются те или иные значения результата внутри интервала, то потребуются аналитические действия над случайными распределениями, там простое сложение двух случайных величин требует вычисления свёртки их плотностей распределения, и вообще всё становится очень сложно и затратно.


        1. Palich239 Автор
          11.03.2019 15:57

          В этом комментарии прекрасно все! Жаль не могу поставить более одного плюсика…