Differential Operator usable in Matrix form, in Python module Sympy
Asked Answered
K

4

5

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?

Kiss answered 17/3, 2013 at 16:50 Comment(5)
Your problem is not with the matrices or with the model that you are implementing, but only about the creation of a differential operator object. The question will be much more valuable if you remove the unnecessary discussion about the model and the matrix.Schwarz
@Krastanov, thank you, the question has been updatedSouse
Multiplying on the right side by D class would work if it was possible to force Python to execute __rmul__ from the right object first than __mul__ from the left object. See question: #5181820Souse
Wy do you include EvalfMixin?Toxic
To really work completely, you would need code.google.com/p/sympy/issues/detail?id=1941. See also github.com/sympy/sympy/wiki/Canonicalization (feel free to edit that page).Toxic
S
6

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://mcmap.net/q/49500/-make-all-symbols-commutative-in-a-sympy-expression
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 )
Souse answered 17/3, 2013 at 16:50 Comment(11)
Without the expand I was left with grouped terms that made harder to apply the treatAdd() and treatMul()Souse
@Toxic Do you know how to avoid args to be sorted? I posted a question for this issue too...Souse
Your method is going to be a mess if you try to add even more functions. Instead of separate functions for each type, create one function with different branches for each type that calls itself recursively. The whole thing will be much simpler.Toxic
thank you, I will keep working on that and then update the post when I get to a better solutionSouse
@Toxic I've updated the answer, it still looks complicated the way these treatExpr(), treatMul() and treatAdd() look like, but it is working now...Souse
Line 60: if expr.args <> (): gives an error for me (python 3.6.5, sympy 1.1.1). Removing the <>and ()fixed that. Also sympy.var(x, y, z, commutative=False) needed to be changed to sympy.var('x', commutative=False) for each variable respectively and subprocess.Popen(...) expected a list of argument-strings. Apart from that it all worked fine. Nice work!Bezanson
@gr4nt3d could you please edit the answer with these corrections? Also, feel free to upvote if you liked this solution ;)Souse
@SaulloG.P.Castro, I am not quite the expert with python OOP, neither with python3 compared to python2 (which the above looks like, based on this super(D, self).__init__()). I will edit these but, would like to point out someone else ought to think about whether its the best way.Bezanson
Even after running evaluateExpr, the expressions do not simplify due to non-commutivity. Would it be possible to turn off commutivity after running evaluateExpr? For example, x*diff(y(x),x)*x doesn't simplify down to x**2*diff(y(x),x).Troth
@Troth probably you can loop through every symbol of the equation using the atom attribute in order to change the commutivity . But do it after the differential operator has been multipliedSouse
@SaulloG.P.Castro SymPy objects are immutable. I found a solution: this answer to the question "Make all symbols commutative in a sympy expression"Troth
S
3

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.

Schwarz answered 17/3, 2013 at 19:39 Comment(8)
Using the D class as a subclass of sympy.Derivative worked for this case (please, see the updated question)Souse
Just subclass Expr, there is no reason (it is actually confusing) to subclass Derivative.Schwarz
And as I already mentioned, your solution will not work for stuff+D. You should have added an answer, not modified the question, so it is easier to comment.Schwarz
The updated question is not the answer, just some more information was included to give some insight about what is needed. The issue that you have pointed out with stuff+D was also verified here. Thank you!Souse
subclassing Expr does really make much more sense. In this way stuff+D does not raise an error, for example, the result of sympy.expand( (D(x)+1/x)*x**2 ) is x**2*D(x) + x. Now I am studying a way to re-make the multiplication in order to call diff again.Souse
Derivatives don't evaluate with subs. You do have to call doit.Toxic
The tip using the expr.doit() method really helped! To make this answer the first to be displayed, it was toggled as the accepted one!Souse
@Krastanov, could you please take a look in the other answer I just posted, I am not sure how to create a doit() inside D to apply the differentiations at the time when it is needed.Souse
T
1

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.

Toxic answered 18/3, 2013 at 17:3 Comment(1)
there is a way to force __mul__ and __rmul__ to be used, as explained by Julien Rioux hereSouse
T
1

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).

Toxic answered 18/3, 2013 at 17:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.