Compute symbolic expressions with fuzzy triangular numbers in python

  • Tutorial
Hello, Habr! Today is a miniature tutorial on how to parse a string with a mathematical expression and calculate it using fuzzy triangular numbers. With appropriate changes to the code, the tutorial will work with other "custom" variables. Help: fuzzy triangular numbers - a special case of fuzzy numbers (fuzzy variables on the numerical axis). I recommend to get acquainted here in more detail here and here .

Requirements:

  • The programming language python 3.x (the code provided in the article was tested on python 3.5)
  • sympy library , can be installed via terminal (console):

    pip install sympy

The procedure for solving the problem:

  1. We connect libraries

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

    Connection fractions is optional, we will use Fraction to store real numbers in the form of a fraction (in order to minimize loss of accuracy). We will use the re library for parsing strings and automatically generating a list of character variables.

    Using the typing library is optional; we use it to explicitly indicate the types of function parameters. The random library will be used to generate test values ​​for fuzzy variables. sympy is a great library for character calculations in Python, with it we will work with the expression string itself.
  2. We describe the class of fuzzy triangular numbers and operations on them. In this example, three operations are enough (addition, subtraction and division). We will introduce operations using the overload of "magic" methods of the corresponding class:

    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)
    

    Forms of representation of fuzzy triangular numbers can be different, we will not go deep. In the presented code, we will pay attention to the methods __add__ (addition operator), __sub__ (subtraction operator), __mul__ (multiplication operator). If you try to add a real number to a fuzzy triangular number, it will be converted into a fuzzy triangular number. A similar situation with a tuple or a list of real numbers - the first three numbers will be perceived as a fuzzy triangular (and also converted to the FuzzyTriangular class). The __pos__ method overrides the unary operator "+". The __neg__ method is a unary "-". The __eq__ method overrides the == operator. If desired, you can additionally redefine operations such as:

    • division
    • exponentiation
    • the absolute value of a number
    • comparisons (more / less, more or equal / less or equal)
    • scalarization (cast to int, float, complex numbers, rounding)
    • inversion and others ...

    You can check the adequacy of the entered operations with a small set of tests, for example, such:

    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)

    These verification operations of addition, division, and multiplication are specified in the code and performed according to the redefinition of the "magic" methods. We would like to be able to carry out the same operations using symbol variables in previously unknown expressions. This requires the introduction of several auxiliary functions.
  3. We introduce auxiliary functions:

    • 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
      We will use this function to search for character variables in an expression string (the default template is a character from A to Z or from a to z and an integer after it up to 2 characters long (or the absence of a number).
    • 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)

      This function allows you to calculate the value of a string expression with substitution instead of symbolic variables variables of any valid type (if the operations contained in the string expression itself are overridden). This is possible thanks to the sympy.lambdify function, which converts a sympy expression into a lambda function that accepts "magic" methods. An important condition for the function to function properly is the correct order of the elements in symbols and values ​​(correspondence of symbols and substituted values).
    • Each time creating a lambda function is expensive. If multiple use of the same expression is required, it is recommended to use the following two functions:

      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)

      The first one returns the lambda function itself, and the second one allows you to calculate the resulting values ​​by substituting a list of values. Once again, attention is focused on the fact that the values ​​used do not have to be triangular fuzzy numbers.

  4. We read the formula line from the file

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

    Something like this can be used as the line formula for the expr.txt file:

    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. We get the character variables from the string expression:

    symbols = symbols_from_expr(expr_str)
    print('AutoSymbols', symbols)
  6. We generate test random triangular numbers:

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

    Sorting random values ​​is required to match the order of the values ​​of the left “0”, center and right “0”.
  7. Convert the formula string to an expression:

    func = lambda_func(expr_str, symbols)
    print('func', '=', func)
  8. We calculate the value of the formula using the lambda function (we use func_subs and expr_subs to make sure the results match):

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

Output Example:

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 =  at 0x06129C00>
func_subs = (-0.391482058715, 0.812813114469, 2.409570627378)
expr_subs = (-0.391482058715, 0.812813114469, 2.409570627378)
[Finished in 1.5s]

The tutorial is over. I hope you find something useful here!

PS: the main "feature" of the described approach is the ability to go beyond the standard types of variables and operations on them for python and sympy. By declaring your class and overloading the "magic" methods, you can compute previously unknown mathematical expressions using sympy (creating lambda functions that accept both standard and user types and operations).

Thanks for attention!

Also popular now: