Repository: sympy/sympy
Pull request: improve assumptions of roots of non-real expression
#### References to other Issues or PRs
Fixes #26545 


#### Brief description of what is fixed or changed
Add a simple check in `_eval_is_extended_real` of the `Pow` class, to return `False`, when the n-th root of a non-real expression is taken, i.e. when the base is not real and the exponent is a rational number with a nominator of `1`.

#### Other comments
The explicit checking, wheather the exponent is `Half` using `if self.exp == S.Half:` seems not optimal. Did i forget other fixed fractions, or is there a better way to check this?

#### Release Notes

<!-- Write the release notes for this release below between the BEGIN and END
statements. The basic format is a bulleted list with the name of the subpackage
and the release note for this PR. For example:

* solvers
  * Added a new solver for logarithmic equations.

* functions
  * Fixed a bug with log of integers. Formerly, `log(-x)` incorrectly gave `-log(x)`.

* physics.units
  * Corrected a semantical error in the conversion between volt and statvolt which
    reported the volt as being larger than the statvolt.

or if no release note(s) should be included use:

NO ENTRY

See https://github.com/sympy/sympy/wiki/Writing-Release-Notes for more
information on how to write release notes. The bot will check your release
notes automatically to see if they are formatted correctly. -->

<!-- BEGIN RELEASE NOTES -->
* core
  * Improve the assumptions system by assuming roots of non-real expressions to be non-real themselves
<!-- END RELEASE NOTES -->


Problem statement:
Roots of non-real elements should be non-real themselves, but their domain is unknown
### Description
This is not a bug, rather an idea for an improvement

At the moment, taking the squareroot of a non-real expression (`is_extended_real==False`) does result in the value of `is_extended_real` being `None`.

```{python}
>>> x = Symbol('x', real=True)
>>> print((I+x).is_extended_real)
False
>>> print(sqrt(I+x).is_extended_real)
None
```
To the best of my knowledge, it would be correct to assume, that the root of a non-real number is not real itself, since this follows directly from the reals being closed under multiplication, i.e. $\sqrt{a}\in \mathbb{R} \Rightarrow \sqrt{a}\cdot \sqrt{a}\in\mathbb{R}$, which contradicts with the assumption.

### Motivation
When solving multivariate quartic polynomials, the solution sometimes contains the above mentioned subexpression.
For example the polynomial $a^4 - 2a^3b - 2a^3c + a^2b^2 + 4a^2bc + a^2c^2 - 2ab^2c - 2abc^2 + b^2c^2 + 1$ has no real solution. When we solve for a, we get, among others, the following solution: $\frac{b}{2} + \frac{c}{2} - \frac{\sqrt{b^2 - 2bc + c^2 - 4I}}{2}$.

```{python}
>>> a = Symbol('a', real=True)
>>> b = Symbol('b', real=True)
>>> c = Symbol('c', real=True)
>>> expr = solve(a**4 - 2*a**3*b - 2*a**3*c + a**2*b**2 + 4*a**2*b*c + a**2*c**2 - 2*a*b**2*c - 2*a*b*c**2 + b**2*c**2 + 1, a)[0]
>>> print(expr.is_extended_real)
None
```

### Possible modification
The function `_eval_is_extended_real` of the `Pow` class could be amplified, to return False, when the base is not real and the exponent is a rational with a nominator of 1.

Hints from issue discussion:
> The function `_eval_is_extended_real` of the `Pow` class could be amplified, to return False, when the base is not real and the exponent is a rational with a nominator of 1.

Sounds reasonable.

Commit message: fix use wrong property

Patch to review:
diff --git a/.mailmap b/.mailmap
index 2b868286fc0f..4c0988c98177 100644
--- a/.mailmap
+++ b/.mailmap
@@ -881,6 +881,7 @@ Liwei Cai <cai.lw123@gmail.com>
 Ljubiša Moćić <3rdslasher@gmail.com>
 Lokesh Sharma <lokeshhsharma@gmail.com> <your_email@youremail.com>
 Longqi Wang <iqgnol@gmail.com>
+Lorenz Winkler <lorenz.winkler@tuwien.ac.at>
 Lorenzo Contento <lorenzo.contento@gmail.com> Lorenzo Contento <lcontento@users.noreply.github.com>
 Lorenzo Contento <lorenzo.contento@gmail.com> lcontento <lcontento@users.noreply.github.com>
 Louis Abraham <louis.abraham@yahoo.fr> <louisabraham@users.noreply.github.com>
diff --git a/sympy/core/power.py b/sympy/core/power.py
index b8676164ec3c..99c74143aa1a 100644
--- a/sympy/core/power.py
+++ b/sympy/core/power.py
@@ -513,6 +513,11 @@ def _eval_is_extended_real(self):
                 return self.exp.is_imaginary
             return
         real_e = self.exp.is_extended_real
+        if real_b == False and real_e:
+            if self.exp == S.Half:
+                return False
+            if type(self.exp) == Rational and self.exp.numerator == 1:
+                return False
         if real_e is None:
             return
         if real_b and real_e:
@@ -1837,6 +1842,6 @@ def _eval_difference_delta(self, n, step):
 power.add((object, object), Pow)
 
 from .add import Add
-from .numbers import Integer
+from .numbers import Integer, Rational
 from .mul import Mul, _keep_coeff
 from .symbol import Symbol, Dummy, symbols


Code review comments:
diff_hunk: @@ -513,6 +513,11 @@ def _eval_is_extended_real(self):
                 return self.exp.is_imaginary
             return
         real_e = self.exp.is_extended_real
+        if real_b == False and real_e:
+            if self.exp == S.Half:
+                return False
+            if type(self.exp) == Rational and self.exp.numerator == 1:
+                return False
path: sympy/core/power.py
text: @user1:
There is already a `if real_b is False and real_e` case below so this should be moved there.

These two cases can be combined with `if isinstance(self.exp, Rational) and self.exp.p == 1`.

Perhaps the code there using `arg` should be removed in favour of this code checking for Rational and numerator 1.

@author:
Hi, thanks for the feedback - i somehow missed that case, sorry.

Regarding the part with `arg`, i would be careful to remove it. As far as i can tell, it is relevant for computing the following:

```{python}
>>> expr = Pow(4*I+4, 8)
>>> print(expr.is_extended_real)
True
```

because when i remove it in favour of my code:
```{python}
>>> expr = Pow(4*I+4, 8)
>>> print(expr.is_extended_real)
None
```

I would thus suggest to not remove it

@user1:
It is expected that the assumptions system cannot resolve all queries and will sometimes return None. The problem with using `arg` here is that it creates a new expression which is not something that should happen within the assumptions system.