Question

We need two matrices of differential operators [B] and [C] such as:

B = sympy.Matrix([[ D(x), D(y) ],
                  [ D(y), D(x) ]])

C = sympy.Matrix([[ D(x), D(y) ]])

ans = B * sympy.Matrix([[x*y**2],
                        [x**2*y]])
print ans
[x**2 + y**2]
[      4*x*y]

ans2 = ans * C
print ans2
[2*x, 2*y]
[4*y, 4*x]

This could also be applied to calculate the curl of a vector field like:

culr  = sympy.Matrix([[ D(x), D(y), D(z) ]])
field = sympy.Matrix([[ x**2*y, x*y*z, -x**2*y**2 ]])

To solve this using Sympy the following Python class had to be created:

import sympy

class D( sympy.Derivative ):
    def __init__( self, var ):
        super( D, self ).__init__()
        self.var = var

    def __mul__(self, other):
        return sympy.diff( other, self.var )

This class alone solves when the matrix of differential operators is multiplying on the left. Here diff is executed only when the function to be differentiated is known.

To workaround when the matrix of differential operators is multiplying on the right, the __mul__ method in the core class Expr had to be changed in the following way:

class Expr(Basic, EvalfMixin):
    # ...
    def __mul__(self, other):
        import sympy
        if other.__class__.__name__ == 'D':
            return sympy.diff( self, other.var )
        else:
            return Mul(self, other)
    #...

It works pretty well, but there should be a better native solution in Sympy to handle this. Does anybody know what it might be?

Was it helpful?

Solution

This solution applies the tips from the other answers and from here. The D operator can be defined as follows:

  • considered only when multiplied from the left, so that D(t)*2*t**3 = 6*t**2 but 2*t**3*D(t) does nothing
  • all the expressions and symbols used with D must have is_commutative = False
  • is evaluated in the context of a given expression using evaluateExpr()
    • which goes from the right to the left along the expression finding the D opperators and applying mydiff()* to the corresponding right portion

*:mydiff is used instead of diff to allow a higher order D to be created, like mydiff(D(t), t) = D(t,t)

The diff inside __mul__() in D was kept for reference only, since in the current solution the evaluateExpr() actually does the differentiation job. A python mudule was created and saved as d.py.

import sympy
from sympy.core.decorators import call_highest_priority
from sympy import Expr, Matrix, Mul, Add, diff
from sympy.core.numbers import Zero

class D(Expr):
    _op_priority = 11.
    is_commutative = False
    def __init__(self, *variables, **assumptions):
        super(D, self).__init__()
        self.evaluate = False
        self.variables = variables

    def __repr__(self):
        return 'D%s' % str(self.variables)

    def __str__(self):
        return self.__repr__()

    @call_highest_priority('__mul__')
    def __rmul__(self, other):
        return Mul(other, self)

    @call_highest_priority('__rmul__')
    def __mul__(self, other):
        if isinstance(other, D):
            variables = self.variables + other.variables
            return D(*variables)
        if isinstance(other, Matrix):
            other_copy = other.copy()
            for i, elem in enumerate(other):
                other_copy[i] = self * elem
            return other_copy

        if self.evaluate:
            return diff(other, *self.variables)
        else:
            return Mul(self, other)

    def __pow__(self, other):
        variables = self.variables
        for i in range(other-1):
            variables += self.variables
        return D(*variables)

def mydiff(expr, *variables):
    if isinstance(expr, D):
        expr.variables += variables
        return D(*expr.variables)
    if isinstance(expr, Matrix):
        expr_copy = expr.copy()
        for i, elem in enumerate(expr):
            expr_copy[i] = diff(elem, *variables)
        return expr_copy
    return diff(expr, *variables)

def evaluateMul(expr):
    end = 0
    if expr.args:
        if isinstance(expr.args[-1], D):
            if len(expr.args[:-1])==1:
                cte = expr.args[0]
                return Zero()
            end = -1
    for i in range(len(expr.args)-1+end, -1, -1):
        arg = expr.args[i]
        if isinstance(arg, Add):
            arg = evaluateAdd(arg)
        if isinstance(arg, Mul):
            arg = evaluateMul(arg)
        if isinstance(arg, D):
            left = Mul(*expr.args[:i])
            right = Mul(*expr.args[i+1:])
            right = mydiff(right, *arg.variables)
            ans = left * right
            return evaluateMul(ans)
    return expr

def evaluateAdd(expr):
    newargs = []
    for arg in expr.args:
        if isinstance(arg, Mul):
            arg = evaluateMul(arg)
        if isinstance(arg, Add):
            arg = evaluateAdd(arg)
        if isinstance(arg, D):
            arg = Zero()
        newargs.append(arg)
    return Add(*newargs)

#courtesy: https://stackoverflow.com/a/48291478/1429450
def disableNonCommutivity(expr):
    replacements = {s: sympy.Dummy(s.name) for s in expr.free_symbols}
    return expr.xreplace(replacements)

def evaluateExpr(expr):
    if isinstance(expr, Matrix):
        for i, elem in enumerate(expr):
            elem = elem.expand()
            expr[i] = evaluateExpr(elem)
        return disableNonCommutivity(expr)
    expr = expr.expand()
    if isinstance(expr, Mul):
        expr = evaluateMul(expr)
    elif isinstance(expr, Add):
        expr = evaluateAdd(expr)
    elif isinstance(expr, D):
        expr = Zero()
    return disableNonCommutivity(expr)

Example 1: curl of a vector field. Note that it is important to define the variables with commutative=False since their order in Mul().args will affect the results, see this other question.

from d import D, evaluateExpr
from sympy import Matrix
sympy.var('x', commutative=False)
sympy.var('y', commutative=False)
sympy.var('z', commutative=False)
curl  = Matrix( [[ D(x), D(y), D(z) ]] )
field = Matrix( [[ x**2*y, x*y*z, -x**2*y**2 ]] )       
evaluateExpr( curl.cross( field ) )
# [-x*y - 2*x**2*y, 2*x*y**2, -x**2 + y*z]

Example 2: Typical Ritz approximation used in structural analysis.

from d import D, evaluateExpr
from sympy import sin, cos, Matrix
sin.is_commutative = False
cos.is_commutative = False
g1 = []
g2 = []
g3 = []
sympy.var('x', commutative=False)
sympy.var('t', commutative=False)
sympy.var('r', commutative=False)
sympy.var('A', commutative=False)
m=5
n=5
for j in xrange(1,n+1):
    for i in xrange(1,m+1):
        g1 += [sin(i*x)*sin(j*t),                 0,                 0]
        g2 += [                0, cos(i*x)*sin(j*t),                 0]
        g3 += [                0,                 0, sin(i*x)*cos(j*t)]
g = Matrix( [g1, g2, g3] )

B = Matrix(\
    [[     D(x),        0,        0],
     [    1/r*A,        0,        0],
     [ 1/r*D(t),        0,        0],
     [        0,     D(x),        0],
     [        0,    1/r*A, 1/r*D(t)],
     [        0, 1/r*D(t), D(x)-1/x],
     [        0,        0,        1],
     [        0,        1,        0]])

ans = evaluateExpr(B*g)

A print_to_file() function has been created to quickly check big expressions.

import sympy
import subprocess
def print_to_file( guy, append=False ):
    flag = 'w'
    if append: flag = 'a'
    outfile = open(r'print.txt', flag)
    outfile.write('\n')
    outfile.write( sympy.pretty(guy, wrap_line=False) )
    outfile.write('\n')
    outfile.close()
    subprocess.Popen( [r'notepad.exe', r'print.txt'] )

print_to_file( B*g )
print_to_file( ans, append=True )

OTHER TIPS

Differential operators do not exist in the core of SymPy, and even if they existed "multiplication by an operator" instead of "application of an operator" is an abuse of notation that is not supported by SymPy.

[1] Another problem is that SymPy expressions can be build only from subclasses of sympy.Basic, so it is probable that your class D simply raises an error when entered as sympy_expr+D(z). This is the reason why (expression*D(z)) * (another_expr) fails. (expression*D(z)) can not be built.

In addition if the argument of D is not a single Symbol it is not clear what you expect from this operator.

Finally, diff(f(x), x) (where f is a symbolic unknown function) returns an unevaluated expressions as you observed simply because when f is unknown there is nothing else that can sensibly returned. Later, when you substitute expr.subs(f(x), sin(x)) the derivative will be evaluate (at worst you might need to call expr.doit()).

[2] There is no elegant and short solution to your problem. A way that I would suggest for solving your problem is to override the __mul__ method of Expr: instead of just multiplying the expression trees it will check if the left expression tree contains instances of D and it will apply them. Obviously this does not scale if you want to add new objects. This is a longstanding known issue with the design of sympy.

EDIT: [1] is necessary simply to permit creation of expressions containing D. [2] is necessary for expressions that containing something more that only one D to work.

If you want right multiplication to work, you'll need to subclass from just object. That will cause x*D to fall back to D.__rmul__. I can't imagine this is high priority, though, as operators are never applied from the right.

Making an operator that works automatically always is not currently possible. To really work completely, you would need http://code.google.com/p/sympy/issues/detail?id=1941. See also https://github.com/sympy/sympy/wiki/Canonicalization (feel free to edit that page).

However, you could make a class that works most of the time using the ideas from that stackoverflow question, and for the cases it doesn't handle, write a simple function that goes through an expression and applies the operator where it hasn't been applied yet.

By the way, one thing to consider with a differential operator as "multiplication" is that it's nonassociative. Namely, (D*f)*g = g*Df, whereas D*(f*g) = g*Df + f*Dg. So you need to be careful when you do stuff that it doesn't "eat" some part of an expression and not the whole thing. For example, D*2*x would give 0 because of this. SymPy everywhere assumes that multiplication is associative, so it's likely to do that incorrectly at some point.

If that becomes an issue, I would recommend dumping the automatic application, and just working with a function that goes through and applies it (which as I noted above, you will need anyway).

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top