Способ 1 (ручной)
Глядя на код, производный класс здесь живет логика верхнего уровня. Это только часть верхнего уровня. С этого момента вычисления требуют вычисления производных различных узлов внутри дерева выражений.
Логика для каждого конкретного узла дерева выражений находится в методе _eval_derivative
a> соответствующий каждому конкретному типу узла.
Это позволит вам добавить код к этим _eval_derivative
методам, чтобы отслеживать весь процесс и находить все шаги.
Способ 2 (с использованием трассировщика)
В Python есть несколько пакетов трассировки. охотник на python, написанный @ionelmc на самом деле довольно хорош и хорошо подходит для этого варианта использования.
Среди многих других функций он позволяет устанавливать определенные обратные вызовы, когда функция начинает выполняться, и еще один, когда функция возвращает свое значение. На самом деле это именно то, что нам нужно.
Вот пример, который показывает, как это использовать (я запускал и тестировал это на Python 3.7.3, SymPy 1.7 и hunter 3.3.1):
import hunter
import sys
from hunter import Q, When, Stop
hunter.trace(
Q(module_contains="sympy",function='_eval_derivative',kind_in=["call","return"],action=hunter.CallPrinter(repr_func=str))
)
from sympy import *
x = symbols('x')
f = 1/(x * sin(x)**2)
f.diff(x)
Таким образом, это позволяет выбрать, какие структуры данных мы хотим проверить, как мы хотим их распечатать, и это позволяет нам увидеть промежуточные этапы процесса дифференциации:
[...]7/site-packages/sympy/core/power.py:1267 call => _eval_derivative(self=sin(x)**(-2), s=x)
[...]7/site-packages/sympy/core/power.py:1267 call => _eval_derivative(self=<sympy.core.power.Pow object at 0x7f5925337150>, s=<sympy.core.symbol.Symbol object at 0x7f5925b6a2b0>)
[...]ite-packages/sympy/core/function.py:598 call => _eval_derivative(self=sin(x), s=x)
[...]ite-packages/sympy/core/function.py:598 call => _eval_derivative(self=<sympy.functions.elementary.trigonometric.sin object at 0x7f592589ee08>, s=<sympy.core.symbol.Symbol object at 0x7f5925b6a2b0>)
[...]ite-packages/sympy/core/function.py:612 return <= _eval_derivative: cos(x)
[...]ite-packages/sympy/core/function.py:612 return <= _eval_derivative: <sympy.functions.elementary.trigonometric.cos object at 0x7f592525fef8>
[...]7/site-packages/sympy/core/power.py:1271 return <= _eval_derivative: -2*cos(x)/sin(x)**3
[...]7/site-packages/sympy/core/power.py:1271 return <= _eval_derivative: <sympy.core.mul.Mul object at 0x7f5925259b48>
[...]7/site-packages/sympy/core/power.py:1267 call => _eval_derivative(self=1/x, s=x)
[...]7/site-packages/sympy/core/power.py:1267 call => _eval_derivative(self=<sympy.core.power.Pow object at 0x7f5925337200>, s=<sympy.core.symbol.Symbol object at 0x7f5925b6a2b0>)
[...]7/site-packages/sympy/core/power.py:1271 return <= _eval_derivative: -1/x**2
[...]7/site-packages/sympy/core/power.py:1271 return <= _eval_derivative: <sympy.core.mul.Mul object at 0x7f5925259f10>
Если вы хотите также охватить функцию diff
, вы можете изменить приведенный выше код и получить function_in=['_eval_derivative','diff']
. Таким образом можно посмотреть не только частичные результаты, но и вызов функции diff
и возвращаемое ею значение.
Способ 3 (использование трассировщика, построение графа вызовов и его визуализация)
Используя graphviz, латекс и трассировщик (опять же, python-hunter), вы действительно можете увидеть граф вызовов более нагляден. Для рендеринга всех формул для каждого промежуточного шага требуется немного времени, потому что используется pdflatex
(хотя я уверен, что для латекса есть более быстрые рендереры).
Значение каждого узла имеет следующий формат:
function_name
argument => return_value
Кажется, есть несколько узлов diff
, у которых аргумент равен возвращаемому значению, которое я не знаю, как объяснить в данный момент.
Диаграмма, вероятно, могла бы быть более полезной, если бы она каким-то образом указывала, где применяется каждое правило (я не могу придумать простого способа сделать это).
Вот код для этого тоже:
import hunter
import sys
from hunter import Q, When, Stop, Action
from hunter.actions import ColorStreamAction
formula_ltx = r'''
\documentclass[border=2pt,varwidth]{letter}
\usepackage{amsmath}
\pagenumbering{gobble}
\begin{document}
\[ \texttt{TITLE} \]
\[ FORMULA \]
\end{document}
'''
# ==============
# == Tracing ===
# ==============
from sympy.printing.latex import LatexPrinter, print_latex, latex
global call_tree_root
# a node object to hold an observed function call
# with its argument, its return value and its function name
class Node(object):
def __init__(self, arg=None, retval=None, func_name=None):
self.arg = arg
self.retval = retval
self.arg_ascii = ""
self.retval_ascii = ""
self.func_name = func_name
self.uid = 0
self.children = []
# this is a hunter action where we build a call graph and populate it
# so we can later render it
#
# CGBAction (Call Graph Builder Action)
class CGBAction(ColorStreamAction):
def __init__(self, *args, **kwargs):
super(ColorStreamAction, self).__init__(*args, **kwargs)
# a custom call stack
self.tstack = []
global call_tree_root
call_tree_root = Node(arg="",func_name="root")
self.node_idx = 1
self.tstack.append(call_tree_root)
def __call__(self, event):
if event.kind in ['return','call']:
if event.kind == 'return':
print(str(event.arg))
if len(self.tstack) > 0:
top = self.tstack.pop()
top.retval = latex(event.arg)
top.retval_ascii = str(event.arg)
elif event.kind == 'call':
print(str(event.locals.get('self')))
new = Node()
new.uid = self.node_idx
new.arg = latex(event.locals.get('self'))
new.arg_ascii = str(event.locals.get('self'))
top = self.tstack[-1]
self.tstack.append(new)
top.children.append(new)
new.func_name = event.module + ":" + event.function
self.node_idx += 1
hunter.trace(
Q(module_contains="sympy",function_in=['_eval_derivative','diff'],kind_in=["call","return"],action=CGBAction)
)
from sympy import *
x = symbols('x')
f = 1 / (x * sin(x)**2)
#f = 1 / (x * 3)
#f = sin(exp(cos(x)*asin(x)))
f.diff(x)
# ============================
# == Call graph rendering ====
# ============================
import os
import re
OUT_DIR="formulas"
if not os.path.exists(OUT_DIR):
os.mkdir(OUT_DIR)
def write_formula(prefix,uid,formula,title):
TEX = uid + prefix + ".tex"
PDF = uid + prefix + ".pdf"
PNG = uid + prefix + ".png"
TEX_PATH = OUT_DIR + "/" + TEX
with open(TEX_PATH,"w") as f:
ll = formula_ltx
ll = ll.replace("FORMULA",formula)
ll = ll.replace("TITLE",title)
f.write(ll)
# compile formula
CMD = """
cd formulas ;
pdflatex {TEX} ;
convert -trim -density 300 {PDF} -quality 90 -colorspace RGB {PNG} ;
""".format(TEX=TEX,PDF=PDF,PNG=PNG)
os.system(CMD)
buf_nodes = ""
buf_edges = ""
def dfs_tree(x):
global buf_nodes, buf_edges
arg = ("" if x.arg is None else x.arg)
rv = ("" if x.retval is None else x.retval)
arg = arg.replace("\r","")
rv = rv.replace("\r","")
formula = arg + "\\Rightarrow " + rv
print(x.func_name + " -> " + x.arg_ascii + " -> " + x.retval_ascii)
x.func_name = x.func_name.replace("_","\\_")
write_formula("",str(x.uid),formula,x.func_name)
buf_nodes += """
{0} [image="{0}.png" label=""];
""".format(x.uid);
for y in x.children:
buf_edges += "{0} -> {1};\n".format(x.uid,y.uid);
dfs_tree(y)
dfs_tree(call_tree_root)
g = open(OUT_DIR + "/graph.dot", "w")
g.write("digraph g{")
g.write(buf_nodes)
g.write(buf_edges)
g.write("}\n")
g.close()
os.system("""cd formulas ; dot -Tpng graph.dot > graph.png ;""")
Сопоставление логики SymPy с правилами дифференциации
Я думаю, что один оставшийся шаг — сопоставить промежуточные узлы из SymPy с правилами дифференциации. Вот некоторые из тех, что мне удалось сопоставить:
Я не видел класса Fraction в sympy.core
, поэтому, возможно, частное правило обрабатывается косвенно через правило продукта и обобщенное правило мощности с показателем -1.
Бег
Чтобы запустить это, вам понадобится:
sudo apt-get install graphviz imagemagick texlive texlive-latex-base
И файл /etc/ImageMagick-6/policy.xml
нужно будет обновить следующей строкой, чтобы разрешить преобразование из PDF->PNG
:
<policy domain="coder" rights="read|write" pattern="PDF" />
Существует еще одна библиотека графиков вызовов, которая называется jonga, но она немного универсальна и не позволяет полностью отфильтровать нежелательные вызовы. звонки.
04.12.2020