Repository: sympy/sympy

Issue:
`CoordSystem._inverse_transformation` raises a ValueError on any input
Here is an example of an inverse transformation between two coordinate systems, where the forward transformation is a quarter-turn around the origin.

```
import sympy.diffgeom


FIRST = 'first'
SECOND = 'second'

m = sympy.diffgeom.Manifold("M", 2)
p = sympy.diffgeom.Patch("P", m)
RELATIONS = {}

x, y = sympy.symbols("x y")
z, w = sympy.symbols("z w")

matrix = sympy.Matrix([y, - x])
RELATIONS[(FIRST, SECOND)] = sympy.Lambda(
    (x, y),
    sympy.Matrix([y, - x])
)

cartesian2D = sympy.diffgeom.CoordSystem(FIRST, p, [x, y], RELATIONS)
cartesian2D_quarter = sympy.diffgeom.CoordSystem(SECOND, p, [z, w], RELATIONS)


if __name__ == "__main__":
    print(sympy.diffgeom.CoordSystem._inverse_transformation(cartesian2D_quarter, cartesian2D))
```

This example gives me this error: 
```
Traceback (most recent call last):
  File "scratch_3.py", line 26, in <module>
    print(sympy.diffgeom.CoordSystem._inverse_transformation(cartesian2D, cartesian2D_quarter))
  File "sympy\sympy\diffgeom\diffgeom.py", line 361, in _inverse_transformation
    forward_syms, forward_results = forward_transform.args
ValueError: too many values to unpack (expected 2)
```
In this example `forward_transform `is `Matrix([[y], [-x]])`, and `forward_transform.args` is `(2, 1, (y, -x))`, so the ValueError is thrown because the left hand side of the assignment in line 361 of diffgeom.py expects a 2-tuple but the right hand side is a 3-tuple.  The ValueError is thrown for any input because forward_transform.args is always a 3-tuple: the first two elements are the shape of the matrix of transformation equations and the third element is the tuple of matrix transformation equations.

There is a related bug around inverting coordinate systems in the CoordSystem.connect_to method: https://github.com/sympy/sympy/issues/6656

Discussion:

Patch:
diff --git a/sympy/diffgeom/diffgeom.py b/sympy/diffgeom/diffgeom.py
index e55dbd5078..ba82beebba 100644
--- a/sympy/diffgeom/diffgeom.py
+++ b/sympy/diffgeom/diffgeom.py
@@ -398,26 +398,30 @@ def transformation(self, sys):
         elif key in self.relations:
             expr = Matrix(self.relations[key])
         elif key[::-1] in self.relations:
-            expr = Matrix(self._inverse_transformations(sys, self))
+            expr = Matrix(self._inverse_transformation(sys, self))
         else:
-            expr = Matrix(self._indirect_transformations(self, sys))
+            expr = Matrix(self._indirect_transformation(self, sys))
         return Lambda(signature, expr)
 
     @staticmethod
     def _inverse_transformation(sys1, sys2):
         # Find the transformation relation from sys2 to sys1
-        forward_transform = sys1.transform(sys2)
-        forward_syms, forward_results = forward_transform.args
+        forward_transform_expressions = sys1.transform(sys2)
 
-        inv_syms = [i.as_dummy() for i in forward_syms]
         inv_results = solve(
-            [t[0] - t[1] for t in zip(inv_syms, forward_results)],
-            list(forward_syms), dict=True)[0]
-        inv_results = [inv_results[s] for s in forward_syms]
+            [t[0] - t[1] for t in zip(sys2.symbols, forward_transform_expressions)],
+            list(sys1.symbols), dict=True)
+        if len(inv_results) == 0:
+            raise NotImplementedError(
+                "Cannot solve inverse of transformation from {} to {}".format(sys1, sys2))
+        elif len(inv_results) > 1:
+            raise ValueError(
+                "Obtained multiple results for inverse of transformation from {} to {}".format(sys1, sys2)
+            )
 
-        signature = tuple(inv_syms)
-        expr = Matrix(inv_results)
-        return Lambda(signature, expr)
+        inv_results = inv_results[0]
+        signature = tuple(sys1.symbols)
+        return [inv_results[s] for s in signature]
 
     @classmethod
     @cacheit
@@ -1837,7 +1841,6 @@ def dummyfy(args, exprs):
     d_exprs = Matrix([_sympify(expr).subs(reps) for expr in exprs])
     return d_args, d_exprs
 
-
 ###############################################################################
 # Helpers
 ###############################################################################


Test patch:
diff --git a/sympy/diffgeom/tests/test_diffgeom.py b/sympy/diffgeom/tests/test_diffgeom.py
index 0c9cc397cb..f5c1209702 100644
--- a/sympy/diffgeom/tests/test_diffgeom.py
+++ b/sympy/diffgeom/tests/test_diffgeom.py
@@ -1,5 +1,5 @@
-from sympy.diffgeom.rn import R2, R2_p, R2_r, R3_r, R3_c, R3_s
-from sympy.diffgeom import (Commutator, Differential, TensorProduct,
+from sympy.diffgeom.rn import R2, R2_p, R2_r, R3_r, R3_c, R3_s, R2_origin
+from sympy.diffgeom import (CoordSystem, Commutator, Differential, TensorProduct,
         WedgeProduct, BaseCovarDerivativeOp, CovarDerivativeOp, LieDerivative,
         covariant_order, contravariant_order, twoform_to_matrix, metric_to_Christoffel_1st,
         metric_to_Christoffel_2nd, metric_to_Riemann_components,
@@ -36,6 +36,46 @@ def test_R2():
         assert m == R2_p.coord_tuple_transform_to(
             R2_r, R2_r.coord_tuple_transform_to(R2_p, m)).applyfunc(simplify)
 
+
+def test_inverse_transformations():
+    p, q, r, s = symbols('p q r s')
+
+    relations_quarter_rotation = {
+        ('first', 'second'): (q, -p)
+    }
+
+    R2_pq = CoordSystem('first', R2_origin, [p, q], relations_quarter_rotation)
+    R2_rs = CoordSystem('second', R2_origin, [r, s], relations_quarter_rotation)
+
+    # The transform method should derive the inverse transformation if not given explicitly
+    assert R2_rs.transform(R2_pq) == Matrix([[-R2_rs.symbols[1]], [R2_rs.symbols[0]]])
+
+    a, b = symbols('a b', positive=True)
+    relations_uninvertible_transformation = {
+        ('first', 'second'): (-a,)
+    }
+
+    R2_a = CoordSystem('first', R2_origin, [a], relations_uninvertible_transformation)
+    R2_b = CoordSystem('second', R2_origin, [b], relations_uninvertible_transformation)
+
+    # The transform method should throw if it cannot invert the coordinate transformation.
+    # This transformation is uninvertible because there is no positive a, b satisfying a = -b
+    with raises(NotImplementedError):
+        R2_b.transform(R2_a)
+
+    c, d = symbols('c d')
+    relations_ambiguous_inverse = {
+        ('first', 'second'): (c**2,)
+    }
+
+    R2_c = CoordSystem('first', R2_origin, [c], relations_ambiguous_inverse)
+    R2_d = CoordSystem('second', R2_origin, [d], relations_ambiguous_inverse)
+
+    # The transform method should throw if it finds multiple inverses for a coordinate transformation.
+    with raises(ValueError):
+        R2_d.transform(R2_c)
+
+
 def test_R3():
     a, b, c = symbols('a b c', positive=True)
     m = Matrix([[a], [b], [c]])