Repository: sympy/sympy (Python)

Issue:
Regression in Matrix indexing with SymPy 1.2
On SymPy 1.1:

```
moorepants@garuda:sympy((sympy-1.1))$ ipython
Python 3.5.5 | packaged by conda-forge | (default, Apr  6 2018, 13:41:05) 
Type 'copyright', 'credits' or 'license' for more information
IPython 6.4.0 -- An enhanced Interactive Python. Type '?' for help.

In [3]: import sympy as sm

In [4]: t = sm.symbols('t')

In [7]: A = sm.Matrix([sm.Function('F')(t), sm.Function('G')(t)])

In [9]: type(A)
Out[9]: sympy.matrices.dense.MutableDenseMatrix

In [10]: type(A.diff(t))
Out[10]: sympy.matrices.dense.MutableDenseMatrix

In [11]: type(A.diff(t)[:])
Out[11]: list
```

On SymPy 1.2:

```
moorepants@garuda:sympy(master)$ ipython
Python 3.5.5 | packaged by conda-forge | (default, Apr  6 2018, 13:41:05) 
Type 'copyright', 'credits' or 'license' for more information
IPython 6.4.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import sympy as sm

In [2]: t = sm.symbols('t')

In [3]: A = sm.Matrix([sm.Function('F')(t), sm.Function('G')(t)])

In [4]: type(A.diff(t))
Out[4]: sympy.matrices.immutable.ImmutableDenseMatrix

In [5]: type(A.diff(t)[:])
Out[5]: tuple

```

MutableDenseMatrix indexing outputs a list now instead of a tuple.

Reported at PyDy here: https://github.com/pydy/pydy/issues/395


Discussion:
Note that both versions output a list for simply created matrix:

```
moorepants@garuda:sympy(master)$ ipython
Python 3.5.5 | packaged by conda-forge | (default, Apr  6 2018, 13:41:05) 
Type 'copyright', 'credits' or 'license' for more information
IPython 6.4.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import sympy as sm

In [2]: A = sm.zeros(2)

In [3]: type(A)
Out[3]: sympy.matrices.dense.MutableDenseMatrix

In [4]: type(A[:])
Out[4]: list

In [5]:                                                                                                  
Do you really want to exit ([y]/n)? 
moorepants@garuda:sympy(master)$ git checkout sympy-1.1
Note: checking out 'sympy-1.1'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at 51be47d70 Change the version in release.py to 1.1 (final)
moorepants@garuda:sympy((sympy-1.1))$ ipython
Python 3.5.5 | packaged by conda-forge | (default, Apr  6 2018, 13:41:05) 
Type 'copyright', 'credits' or 'license' for more information
IPython 6.4.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import sympy as sm

In [2]: A = sm.zeros(2)

In [3]: type(A)
Out[3]: sympy.matrices.dense.MutableDenseMatrix

In [4]: type(A[:])
Out[4]: list
```
It seems that the diff() method on MutableDenseMatrix now outputs an ImutableDenseMatrix.
This is the commit that introduced the regression: https://github.com/sympy/sympy/pull/13641/commits/2831e344ffc1f7b6a3874ec96331de93f7bf665e

And the PR:

https://github.com/sympy/sympy/pull/13641
CC @upabjojr
Do you need this fixed quickly?
I'm getting errors in my downstream package resonance based on this regression. `diff()` changes the type to an immutable matrix and if you rely on the methods for a mutable matrix (as has been in the past) code breaks after you introduce the `.diff()`. This needs to be patched to return the old behavior.
Can we revert the PR that introduced this change? It is breaking downstream code that relies on SymPy mechanics. I'm seeing it across a number of my projects.
Worth noting that this regression is in SymPy 1.2 and 1.3. It would be nice if a 1.2.1 and 1.3.1 fixed it for both versions.



Patch:
diff --git a/doc/src/modules/physics/mechanics/examples/rollingdisc_example_lagrange.rst b/doc/src/modules/physics/mechanics/examples/rollingdisc_example_lagrange.rst
index a5a19c15b355..b5d532782fd7 100644
--- a/doc/src/modules/physics/mechanics/examples/rollingdisc_example_lagrange.rst
+++ b/doc/src/modules/physics/mechanics/examples/rollingdisc_example_lagrange.rst
@@ -56,14 +56,12 @@ accelerations(q double dots) with the ``rhs`` method. ::
   >>> q = [q1, q2, q3]
   >>> l = LagrangesMethod(Lag, q)
   >>> le = l.form_lagranges_equations()
-  >>> le = le.simplify()
-  >>> le
+  >>> le.simplify(); le
   Matrix([
   [m*r**2*(6*sin(q2)*q3'' + 5*sin(2*q2)*q1'*q2' + 6*cos(q2)*q2'*q3' - 5*cos(2*q2)*q1''/2 + 7*q1''/2)/4],
   [                      m*r*(4*g*sin(q2) - 5*r*sin(2*q2)*q1'**2/2 - 6*r*cos(q2)*q1'*q3' + 5*r*q2'')/4],
   [                                                 3*m*r**2*(sin(q2)*q1'' + cos(q2)*q1'*q2' + q3'')/2]])
-  >>> lrhs = l.rhs().simplify()
-  >>> lrhs
+  >>> lrhs = l.rhs(); lrhs.simplify(); lrhs
   Matrix([
   [                                                          q1'],
   [                                                          q2'],
diff --git a/sympy/matrices/matrices.py b/sympy/matrices/matrices.py
index 1d749498bdf7..f5eab5615e95 100644
--- a/sympy/matrices/matrices.py
+++ b/sympy/matrices/matrices.py
@@ -1709,7 +1709,11 @@ def diff(self, *args, **kwargs):
         # XXX this should be handled here rather than in Derivative
         from sympy import Derivative
         kwargs.setdefault('evaluate', True)
-        return Derivative(self, *args, **kwargs)
+        deriv = Derivative(self, *args, evaluate=True)
+        if not isinstance(self, Basic):
+            return deriv.as_mutable()
+        else:
+            return deriv
 
     def _eval_derivative(self, arg):
         return self.applyfunc(lambda x: x.diff(arg))


Test patch:
diff --git a/sympy/matrices/tests/test_matrices.py b/sympy/matrices/tests/test_matrices.py
index 8b4a27abb797..d82aeeb017d9 100644
--- a/sympy/matrices/tests/test_matrices.py
+++ b/sympy/matrices/tests/test_matrices.py
@@ -1931,6 +1931,7 @@ def test_limit():
 
 def test_diff():
     A = MutableDenseMatrix(((1, 4, x), (y, 2, 4), (10, 5, x**2 + 1)))
+    assert isinstance(A.diff(x), type(A))
     assert A.diff(x) == MutableDenseMatrix(((0, 0, 1), (0, 0, 0), (0, 0, 2*x)))
     assert A.diff(y) == MutableDenseMatrix(((0, 0, 0), (1, 0, 0), (0, 0, 0)))
 
@@ -1938,12 +1939,16 @@ def test_diff():
     assert diff(A, y) == MutableDenseMatrix(((0, 0, 0), (1, 0, 0), (0, 0, 0)))
 
     A_imm = A.as_immutable()
+    assert isinstance(A_imm.diff(x), type(A_imm))
     assert A_imm.diff(x) == ImmutableDenseMatrix(((0, 0, 1), (0, 0, 0), (0, 0, 2*x)))
     assert A_imm.diff(y) == ImmutableDenseMatrix(((0, 0, 0), (1, 0, 0), (0, 0, 0)))
 
     assert diff(A_imm, x) == ImmutableDenseMatrix(((0, 0, 1), (0, 0, 0), (0, 0, 2*x)))
     assert diff(A_imm, y) == ImmutableDenseMatrix(((0, 0, 0), (1, 0, 0), (0, 0, 0)))
 
+
+def test_diff_by_matrix():
+
     # Derive matrix by matrix:
 
     A = MutableDenseMatrix([[x, y], [z, t]])
diff --git a/sympy/physics/mechanics/tests/test_lagrange.py b/sympy/physics/mechanics/tests/test_lagrange.py
index 8cc3e2c23efb..038c23fea5fa 100644
--- a/sympy/physics/mechanics/tests/test_lagrange.py
+++ b/sympy/physics/mechanics/tests/test_lagrange.py
@@ -216,11 +216,11 @@ def test_rolling_disc():
     q3 = Function('q3')
     l = LagrangesMethod(Lag, q)
     l.form_lagranges_equations()
-    RHS = l.rhs().as_mutable()
+    RHS = l.rhs()
     RHS.simplify()
     t = symbols('t')
 
-    assert tuple(l.mass_matrix[3:6]) == (0, 5*m*r**2/4, 0)
+    assert (l.mass_matrix[3:6] == [0, 5*m*r**2/4, 0])
     assert RHS[4].simplify() == (
         (-8*g*sin(q2(t)) + r*(5*sin(2*q2(t))*Derivative(q1(t), t) +
         12*cos(q2(t))*Derivative(q3(t), t))*Derivative(q1(t), t))/(10*r))