Repository: sympy/sympy

Issue:
v1.7: solve(set) incorrect for some Piecewise equations
The following behavior was introduced in 1.7.

`simplify` in some circumstances transforms `sign(x)` into a `Piecewise` involving `x  / Abs(x)`; somehow this transformed expression can lead to incorrect solutions from `solve` and `solveset`.

Consider, e.g., `f(x) = x * sign(x)`, which has a zero at `x == 0`. After simplifying, even though the `Piecewise` function explicitly states the case `f(x) = 0` for `x = 0`, `solveset` does not find this as a root:

```python
>>> import sympy as sym
>>> x = sym.Symbol("x", real=True)
>>> func = x * sym.sign(x)
>>> sym.solveset(sym.Eq(func, 0), x, sym.S.Reals)
{0}
>>> sym.simplify(func)
Piecewise((0, Eq(x, 0)), (x**2/Abs(x), True))
>>> sym.solveset(sym.Eq(sym.simplify(func), 0), x, sym.S.Reals)
EmptySet
```

and likewise for `solve`. I'm unsure whether the change introduced in 1.7 was `simplify`'s handling of `sign` or something internal to the solvers.

Of course, this isn't the case for all piecewise functions, e.g.,

```python
>>> sym.solveset(sym.Piecewise((0, sym.Eq(x, 0)), (x**2 + 1, sym.Ne(x, 0))), x, sym.Reals)
{0}
```

It seems it could be unique to having the `Abs` in the denominator. To be clear, I don't think it's specific to `sign` - this was just how I encountered the error and it illustrates that solve[set]'s results can change just by simplifying input.

Discussion:

Patch:
diff --git a/sympy/solvers/solveset.py b/sympy/solvers/solveset.py
index 4afadae57b..42d105481c 100644
--- a/sympy/solvers/solveset.py
+++ b/sympy/solvers/solveset.py
@@ -439,7 +439,27 @@ def _domain_check(f, symbol, p):
         return True
     elif f.subs(symbol, p).is_infinite:
         return False
+    elif isinstance(f, Piecewise):
+        # Check the cases of the Piecewise in turn. There might be invalid
+        # expressions in later cases that don't apply e.g.
+        #    solveset(Piecewise((0, Eq(x, 0)), (1/x, True)), x)
+        for expr, cond in f.args:
+            condsubs = cond.subs(symbol, p)
+            if condsubs is S.false:
+                continue
+            elif condsubs is S.true:
+                return _domain_check(expr, symbol, p)
+            else:
+                # We don't know which case of the Piecewise holds. On this
+                # basis we cannot decide whether any solution is in or out of
+                # the domain. Ideally this function would allow returning a
+                # symbolic condition for the validity of the solution that
+                # could be handled in the calling code. In the mean time we'll
+                # give this particular solution the benefit of the doubt and
+                # let it pass.
+                return True
     else:
+        # TODO : We should not blindly recurse through all args of arbitrary expressions like this
         return all([_domain_check(g, symbol, p)
                     for g in f.args])
 


Test patch:
diff --git a/sympy/solvers/tests/test_solveset.py b/sympy/solvers/tests/test_solveset.py
index 5a2c04bfe8..bb8154acd5 100644
--- a/sympy/solvers/tests/test_solveset.py
+++ b/sympy/solvers/tests/test_solveset.py
@@ -702,6 +702,12 @@ def test_piecewise_solveset():
     f = BooleanTrue()
     assert solveset(f, x, domain=Interval(-3, 10)) == Interval(-3, 10)
 
+    # issue 20552
+    f = Piecewise((0, Eq(x, 0)), (x**2/Abs(x), True))
+    g = Piecewise((0, Eq(x, pi)), ((x - pi)/sin(x), True))
+    assert solveset(f, x, domain=S.Reals) == FiniteSet(0)
+    assert solveset(g) == FiniteSet(pi)
+
 
 def test_solveset_complex_polynomial():
     assert solveset_complex(a*x**2 + b*x + c, x) == \
@@ -2474,16 +2480,6 @@ def test_issue_17276():
      FiniteSet((5**(S(1)/5), 25*5**(S(3)/10)))
 
 
-def test_issue_10426():
-    x=Dummy('x')
-    a=Symbol('a')
-    n=Dummy('n')
-    assert (solveset(sin(x + a) - sin(x), a)).dummy_eq(Dummy('x')) == (Union(
-        ImageSet(Lambda(n, 2*n*pi), S.Integers),
-        Intersection(S.Complexes, ImageSet(Lambda(n, -I*(I*(2*n*pi + arg(-exp(-2*I*x))) + 2*im(x))),
-        S.Integers)))).dummy_eq(Dummy('x,n'))
-
-
 @XFAIL
 def test_substitution_with_infeasible_solution():
     a00, a01, a10, a11, l0, l1, l2, l3, m0, m1, m2, m3, m4, m5, m6, m7, c00, c01, c10, c11, p00, p01, p10, p11 = symbols(