Step by step differentiation with sympy

我只是一个虾纸丫 提交于 2021-02-08 10:10:20

问题


I'm trying to make a python proram to find derivatives and integrals as well as showing how. I have so far found that there is an integral_steps function which returns the steps used, but I have not found an equivalent for differentiation.

Does anyone know if there is an equivalent?

If there isn't, do you have any ideas on how to find the steps needed to find a derivative?


回答1:


Method 1 (manual)

Looking at the code, the Derivative class is where the top-level logic lives. That's only the top-level part. From there on, the computation requires computing derivatives of different nodes inside the expression tree.

The logic for each specific node of the expression tree lives in the _eval_derivative method corresponding to each particular node type.

This would allow you to add code to those _eval_derivative methods in order to trace the entire process and find all the steps.

Method 2 (using a tracer)

Python has multiple tracing packages. python-hunter written by @ionelmc is quite good actually and fits this use-case well.

Among many other features, it allows installing certain callbacks when a function starts executing, and another one when the function returns its value. In fact that's exactly what we need.

Here's an example that shows how to use this (I ran and tested this on Python 3.7.3, SymPy 1.7 and 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)

So this allows to pick which data structures we want to inspect, how we want to print them, and it allows us to see the intermediary steps of the differentiation process:

[...]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>

If you want to also cover the diff function you can alter the code above and have function_in=['_eval_derivative','diff'] . In this way, you can look at not only the partial results, but also the call of the diff function and its return value.

Method 3 (using a tracer, building a call graph and visualizing it)

Using graphviz, latex and a tracer (again, python-hunter) you can actually see the call graph more clearly. It does take a bit of time to render all the formulas for each intermediary step, because pdflatex is being used (I'm sure there's faster renderers for latex though).

Each node's value is in the following format:

function_name
argument => return_value

There seem to be a few diff nodes that have the argument equal to the return value which I'm not sure how to explain at the moment.

The diagram could probably be more useful if it mentioned somehow where each rule was applied (I can't think of an easy way to do that).

Here's the code for this too:

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 ;""")

Mapping SymPy logic to differentiation rules

I think one remaining step is to map intermediary nodes from SymPy to differentiation rules. Here's some of the ones I was able to map:

  • Product rule maps to sympy.core.mul.Mul._eval_derivative
  • Chain rule maps to sympy.core.function.Function._eval_derivative
  • Sum rule maps to sympy.core.add.Add._eval_derivative
  • Generalized power rule maps to sympy.core.power.Pow._eval_derivative

I haven't seen a Fraction class in sympy.core so maybe the quotient rule is handled indirectly through a product rule, and a generalized power rule with exponent -1.

Running

In order to get this to run you'll need:

sudo apt-get install graphviz imagemagick texlive texlive-latex-base

And the file /etc/ImageMagick-6/policy.xml will have to be updated with the following line to allow conversion fron PDF->PNG:

<policy domain="coder" rights="read|write" pattern="PDF" />

There's another call graph library called jonga but it's a bit generic and doesn't allow to completely filter out unwanted calls.



来源:https://stackoverflow.com/questions/64943719/step-by-step-differentiation-with-sympy

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!