image

Реализация алгоритмов на языке Python с использованием символьных вычислений и интерпретируемого языка очень удобна при решении задач математического моделирования процессов и объектов. Основываясь на библиотеке SymPy, Python с успехом справляется с решением уравнений и систем, интегрированием и дифференцированием, вычислением пределов, разложением в ряд и суммированием рядов, упрощением выражений, выполняет поиск решения дифференциальных уравнений и систем.

При использовании символьных вычислений пользователю предоставляется возможность управлять работой программы в процессе ее исполнения путём ввода любых допустимых функций с заданным количеством переменных.

Как преподаватель дисциплины «Компьютерная техника и программирование», в модуле, посвященном программированию на языке Python, я знакомлю студентов с возможностями этого языка для научных исследований. Вашему вниманию представляется цикл статей, в которых можно ознакомиться с символьными вычислениями на Python. Хочу сразу предупредить, что данные статьи не претендуют на абсолютную уникальность, так как собраны на основании материалов из различных источников, их цель – обучить студентов основам символьных вычислений.

Самым первым шагом на пути к символьным вычислениям является импортирование функций модуля SymPy с помощью pip, системы управления пакетами Python. Если вы с этим справились, сразу перейдем к объявлению переменных.

Примечание. Для сокращения записи во всех следующих примерах не приводится первая строка: from sympy import *

Явное объявление символьных переменных


Для символьных вычислений с помощью модуля SymPy символьные переменные и функции должны быть объявлены как таковые. В программах для математических вычислений, таких как Mathematica или Maple, переменные сразу рассматриваются как символьные. В Python же их необходимо принудительно объявить символьными, и сделать это можно несколькими путями. Самым простым будет использование функций symbols() или var(). Первая функция возвращает ссылку на символьный объект в виде какой-либо переменной. Вторая, без присваивания создает символьную переменную.

Пример кода
>>> x,y,a,b = symbols('x y a b') # созданы четыре символьные переменные, предыдущие же значения переменных затираются
>>> f=a**3*x + 3*a**2*x**2/2 + a*x**3 + x**4/4 # переменная f становится автоматически символьной 
>>> type(f)
<class 'sympy.core.add.Add'>
>>> var('u,v')
(u, v)
>>> f=sin(u)**2+tan(v) # переменная f автоматически становится символьной
>>> type(f)
<class 'sympy.core.add.Add'>


Главное отличие между функциями symbols() и var() состоит в том, первая функция возвращает ссылку на символьный объект. Для использования в дальнейшем, ее нужно присвоить какой-либо переменной. Вторая, без присваивания, создает символьную переменную.
В функциях symbols() и var() можно объявлять символьные переменные с индексом:

Пример кода
>>> x=symbols('x:9'); x  # диапазон индексов от 0 до 9
(x0, x1, x2, x3, x4, x5, x6, x7, x8)
>>> x=symbols('x5:10'); x  # диапазон индексов от 5 до 9
(x5, x6, x7, x8, x9)
>>> x=var('x:9'); x   # диапазон индексов от 0 до 9
(x0, x1, x2, x3, x4, x5, x6, x7, x8)
>>> x=var('x5:10'); x   # диапазон индексов от 5 до 9
(x5, x6, x7, x8, x9)


Также можно назначить тип и накладывать ограничения на символьные переменные прямо в функциях symbols() и var(). Иногда без таких ограничений очевидные преобразования не работают, например, сравните:

Пример кода
>>> x = symbols('x', integer=True) #назначаем целый тип 
>>> sqrt(x**2)
Abs(x)
>>> x = symbols('x', positive = True, integer=True)
>>> sqrt(x**2)
x
>>> x = symbols('x')
>>> sqrt(x**2) # это x, если x?0
sqrt(x**2)

>>> x = var('x', integer=True)
>>> sqrt(x**2)
Abs(x)
>>> x = var('x', positive = True, integer=True)
>>> sqrt(x**2)
x
>>> x =  var('x')
>>> sqrt(x**2) # это x, если x?0
sqrt(x**2)


Чтобы создать контейнер для одиночного символа, используем аргумент seq=True:

>>> symbols('x',seq=True)
(x,)

Определение действительных значений для символьных переменных:

>>> x, y, z = symbols('x,y,z', real=True)
>>> x.is_real and y.is_real and z.is_real
True

Функция S()


Иногда символьные выражения могут быть проинтерпретированы как числовые константы Python, а не SymPy. Поэтому для объявления символьных переменных, а также для преобразования числовых констант в символьные, применяют функцию S(), например, сравним:

>>> expr = x**2 + sin(y) + S(10)/2; expr
x**2 + sin(y) + 5 
>>> type(10)
<class 'int'>
>>> type(S(10)) # символьная константа десять
<class 'sympy.core.numbers.Integer'>

Разница между постоянной Python и символьной состоит в том, что символьная константа может быть вычислена с заданной степенью точности, как показано в следующем примере в сравнении со стандартной функцией round():

z=1/7; z # вычисляет переменную z с процессорной точностью
0.14285714285714285
z1=S(1)/7; z1
1/7
z2=z1.n(30); z2 # вычисляет переменную z2 с точностью до 30 значащих цифр
0.142857142857142857142857142857
z3=round(z1,30); z3
0.14285714285714285

Cимвольные имена


Если в текущей сессии необходимо использовать символьную математику постоянно, то можно импортировать общепринятые символьные имена из модуля sympy.abc:

Пример кода
>>> import sympy.abc 
>>> dir(sympy.abc)
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '_clash', '_clash1', '_clash2', 'a', 'alpha', 'b', 'beta', 'c', 'chi', 'd', 'delta', 'division', 'e', 'epsilon', 'eta', 'exec_', 'f', 'g', 'gamma', 'greeks', 'h', 'i', 'iota', 'j', 'k', 'kappa', 'l', 'lamda', 'm', 'mu', 'n', 'nu', 'o', 'omega', 'omicron', 'p', 'phi', 'pi', 'print_function', 'psi', 'q', 'r', 'rho', 's', 'sigma', 'string', 'symbols', 't', 'tau', 'theta', 'u', 'upsilon', 'v', 'w', 'x', 'xi', 'y', 'z', 'zeta']


Имя переменной из пространства имен можно удалить командой del имя1, имя2,..:

>>> type(x)
<class 'sympy.core.symbol.Symbol'>
>>> del x,y 
>>> x
NameError: name 'x' is not defined 

Для восстановления значений стандартных констант, а также имен некоторых функций, нужно повторно загрузить модуль sympy.

>>> from sympy import *

Метод subs(...)


Следует помнить, что при записи символьного выражения может автоматически выполняться его упрощение, например:

>>> a,b,c,d,x,y,z,u,v,w = symbols('a b c d x y z u v w') 
>>> x - z + 20 -z- 15 + 3*sin(pi/2)+2*z
x + 8

Метод subs(...) используется для вычисления символьного выражения при заданных значениях переменных, например:

>>> a, x = symbols('a x')
>>> f= a**3*x + 3*a**2*x**2/2 + a*x**3 + x**4/4 
>>> f.subs(a,1) # в выражение f вместо переменной a была подставлена единица
x**4/4 + x**3 + 3*x**2/2 + x

Если в методе subs использовать два аргумента, то они интерпретируются как subs(old,new), т.е. старый идентификатор old заменяется новым new. Аргумент метода subs() может быть последовательностью, которая должна содержать пары (old,new), а может быть символьным выражением, например:

>>> a,b,c,d,x,y,z = symbols('a b c d x y z')
>>> f=a*x**3 +b*y**2 + c*z+d
>>> f.subs([(a,1),(b,2),(c,3),(d,4)]) # выполнена подстановка a=1, b=2, c=3, d=4
x**3 + 2*y**2 + 3*z + 4
>>> pr= x**3+4*x**2+6*x+10 
>>> pr.subs(x,1/x) # выполнена подстановка символьного выражения
10 + 6/x + 4/x**2 + x**(-3)

Обратим ваше внимание на следующую особенность работы с переменными (символьными и обычными переменными Python). Выполним следующий код:

>>> x='Hello' 
>>> pr=x+'world' 
>>> pr 
'Helloworld' 
>>> x='AAA' #присвоили символьной переменной x новое значение
>>> pr
'Helloworld'

Здесь действует правило: если переменная изменилась, то созданное ранее выражение, содержащее эту переменную, не пересчитывается автоматически. Это правило срабатывает и для обычных переменных Python.

Операции с дробями


Модуль SymPy может проводить вычисления с дробями и приводить их к общему знаменателю, например, сравните:

>>> S(1)/3+S(2)/5
11/15 
>>> 1/3+2/5
0.7333333333333334

Функции Rational(числитель, знаменатель) и Integer(...) используются для создания рациональных дробей без десятичного округления:

>>> z=Rational(1, 7)+Rational(2, 5); z 
19/35
>>> Integer(1)/Integer(5)
1/5
>>> 1/5
0.2
>>> z=Integer(1)/Integer(5)+Rational(2, 7); z
17/35

Округления вычислений


В символьных вычислениях работает правило – если ничего не сказано, не делать никаких округлений. Посмотрите, как в первом случае Python преобразует выражение, но оставит в записи ответа квадратный корень и не выполнит никаких округлений, а во втором, так как одно из чисел задано с десятичной точкой, результат будет приближенным:

>>> sqrt(20)  
2*sqrt(5)
>>> sqrt(20.0) # в выражении используется число с десятичной точкой
4.47213595499958

Для любого символьного объекта существует метод evalf(...)(evaluate float), который возвращает его десятичное представление:

>>> sqrt(20).evalf()  # функция sqrt() модуля sympy
4.47213595499958
>>> E.evalf()
2.71828182845905

В методе evalf([n,...]) можно использовать аргумент, задающий точность результата (n = количество значащих цифр)

>>> sqrt(20).evalf(30)
4.47213595499957939281834733746
>>> pi.evalf(20)
3.1415926535897932385

Также всегда нужно помнить, что вещественная арифметика не возвращает точный результат, сравните:

>>> from sympy import *
>>> one=S('one')
>>> one = cos(1)**2 + sin(1)**2 
>>> one.evalf() # равно 1
1.00000000000000
>>> (one-1).evalf() # должно быть равно 0
-0.e-124 

Если известно, что результат содержит погрешность вычислений, то с помощью опции chop=True метода evalf() ее можно удалить. Очень маленькое значение вещественной или мнимой части результата в этом случае заменяется нулем. Возьмем предыдущий пример:

>>> (one-1).evalf() # должно быть равно 0
-0.e-124 
>>> (one - 1).evalf(chop=True)
0

Бесконечность


После выполнения первой строки from sympy import * становится доступен символ бесконечности – oo (две буквы „o?), с которым тоже можно выполнять определенные операции:

>>> oo+1
oo
>>> 1000000<oo
True
>>> 1/oo
0

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

Вывод


Рассмотренные в статье символьные вычисления отличаются от числовых методов тем, что результаты можно и дальше исследовать, например, определять экстремумы функций, решать уравнения со вложенными переменными и так далее.

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

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


  1. amarao
    27.09.2018 15:03

    Спасибо.

    Его интеграция с jupyter — считай, замета маткаду (плюс-минус).


    1. netricks
      27.09.2018 18:39
      +1

      И в отличии от традиционных мат пакетов не весит, непонятно зачем, кучу гигабайт. И можно прям в эмбедед изделие символьное ядро внедрять.


      1. slovak
        27.09.2018 22:11

        >> непонятно зачем, кучу гигабайт

        То, что не понятно Вам, может иметь вполне простое объяснение.
        Ну вот я вывел в SymPy символьное уравнение на 100500 слагаемых. Как от этого результата перейти к оптимизированной реализации с subexpression extraction, например, а потом функцию получилть на С++ или С?

        Несколько раз пытался пересесть с MATLAB Symbolic Toolbox на SymPy, но все старания разбивались о академичный уклон последнего. При этом пакеты на 10+ гигабайт это умеют, и очень давно, и очень трудно от этого блага отказаться в пользу opensourse.

        По поводу символьного ядра в ембеддед даже трудно представляю себе юз кейс.


        1. homocomputeris
          27.09.2018 23:11

          Использовать Codegen.
          А Матлаб до сих пор считает, что нулевой полином Чебышёва — это ноль?


          1. slovak
            28.09.2018 10:32

            Я не являюсь фанатом MATLAB, несмотря на многолетний опыт работы, колкое замечание по поводу полиномов Чебышева не к месту. Напротив, как я ранее выразился — я делал несколько попыток заменить проприетарный инструмент на опен сорс. Это чуть ли не последний инструмент в моем наборе, для котороне не удалось найти замену.
            Подскажите, Вы использовали в боевых условиях кодген от NumPy? Или просто погуглили? В нижеприведенном видео неплохо представлен функционал, с которым я пыталася совладать. Выглядит сносно, но достаточно немного отойти в сторону от простых примеров. А именно:
            1. Масштабные выражения и их оптимизация с точки зрения вычислительной сложности (subexpression extraction and reuse, reduced precision and bit depth, fixed point conversion).
            2. Работа с входными параметрами и локальными переменными, const, pointer, ref, etc…
            3. Code replacement, это когда у Вас аппаратное обеспечение предоставляет быстрое матричное умножение, и вы хотите, чтобы кодген использовал Вашу функцию. Заметте, это не просто пройтись по уже сгенерированному файлу и заменить. Это должно работать на уровне выражений.


            1. homocomputeris
              28.09.2018 12:30

              Мой комментарий был к тому, что и Матлаб надо было дорабатывать напильником.
              Нет, не использовал. Думаю, что в какие-то функции ещё в зачаточном состоянии, и если вы хотите писать символьный код, а исполнять его в плюсах, то нужно придётся использовать Матлаб или что-нибудь вроде SymEngine.
              Опять же предположу, что SymPy сейчас имеет другие сценарии использования, например, бесшовная интеграция с NumPy и SciPy.


  1. LinearLeopard
    27.09.2018 17:53

    А математику с пределами, производными, интегралами и прочими вычетами оставили на потом?

    Интересно, насколько это быстрее/медленнее аналогов.


  1. maisvendoo
    28.09.2018 07:26

    Ну что, написано очень добротно, а главное без претензий на научные откровения вселенского масштаба, что не может не радовать. Цель четко обозначена: «обучить студентов основам символьных вычислений» и текст строго следует этой цели. Видимо, Юрий Карлович переосмыслил свой подход к популяризации питона и нашел достойного ученика. Меня это радует, желаю успехов Вам на ресурсе


    1. helenblack Автор
      29.09.2018 14:10

      Спасибо


  1. homocomputeris
    28.09.2018 12:33

    В любой статье про SymPy нужно писать версию, потому что всё довольно таки подвижно. Например, до 1.3 некоммутирующие переменные не работали.


  1. sena
    28.09.2018 15:15

    А его можно научить работать с гиперреальными числами, инфинитисами и прочим нестандартным вычислениям?