Repository: sympy/sympy

Issue:
Lazy eval of `CRootOf` on irreducibles with negative lead coeff skips normalization
## Example

```python
In [1]: from sympy.abc import x

In [2]: from sympy import CRootOf

In [3]: g = -x**2 + 2

In [4]: s = CRootOf(g, 0)

In [5]: s.n()
...
KeyError: PurePoly(-x**2 + 2, x, domain='ZZ')
```

## Explanation

If `f` is an irreducible poly, and you construct a `CRootOf` for it,
```python
r = CRootOf(f, 0)
```
then `r.poly` is not always equal to `f`. It is a normalized version, in which some content has been extracted:
```python
In [8]: f = 2*x**2 - 4

In [9]: r = CRootOf(f, 0)

In [10]: r.poly
Out[10]: PurePoly(x**2 - 2, x, domain='ZZ')
```
Later, after some task causes the cache to be populated, the *same* normalized poly is used as cache key:
```python
In [14]: r.n()
Out[14]: -1.41421356237310

In [16]: rootoftools._reals_cache._dict.keys()
Out[16]: dict_keys([PurePoly(x**2 - 2, x, domain='ZZ')])
```

However, if the lead coeff of the poly is negative, then at construction time this is *not* extracted as part of the content, but it *is* extracted later, when the poly is used as cache key:
```python
In [17]: g = -x**2 + 2

In [18]: s = CRootOf(g, 0)

In [19]: s.poly
Out[19]: PurePoly(-x**2 + 2, x, domain='ZZ')

In [20]: CRootOf.clear_cache()

In [21]: s._ensure_reals_init()

In [22]: rootoftools._reals_cache._dict.keys()
Out[22]: dict_keys([PurePoly(x**2 - 2, x, domain='ZZ')])
```
This means we get a `KeyError` the moment we need to use the cache:
```python
In [23]: s.n()
...
KeyError: PurePoly(-x**2 + 2, x, domain='ZZ')
```

## Diagnosis

The new lazy eval (since #23426)

https://github.com/sympy/sympy/blob/6f2e2192d07b7bdf239b8b48b9b3afadc5cb42e0/sympy/polys/rootoftools.py#L641-L649

returns `poly, index`, but `poly` is not the normalized version. The normalized version is `factors[0][0]`,
because it's `_pure_factor()` that extracts a factor of `-1`, when the lead coeff is negative.

The reason everything is fine with other, *non-negative* content, is because *that* content is extracted earlier, when `CRootOf.__new__()` makes a call to `preprocess_roots()`. So, that happens whether it's the lazy case or not.

Discussion:

Patch:
diff --git a/sympy/polys/rootoftools.py b/sympy/polys/rootoftools.py
index e7acf4e11f..aea9ed2869 100644
--- a/sympy/polys/rootoftools.py
+++ b/sympy/polys/rootoftools.py
@@ -645,8 +645,11 @@ def _indexed_root(cls, poly, index, lazy=False):
         # If the given poly is already irreducible, then the index does not
         # need to be adjusted, and we can postpone the heavy lifting of
         # computing and refining isolating intervals until that is needed.
+        # Note, however, that `_pure_factors()` extracts a negative leading
+        # coeff if present, so `factors[0][0]` may differ from `poly`, and
+        # is the "normalized" version of `poly` that we must return.
         if lazy and len(factors) == 1 and factors[0][1] == 1:
-            return poly, index
+            return factors[0][0], index
 
         reals = cls._get_reals(factors)
         reals_count = cls._count_roots(reals)


Test patch:
diff --git a/sympy/polys/tests/test_rootoftools.py b/sympy/polys/tests/test_rootoftools.py
index 418ae9074b..f71ee97899 100644
--- a/sympy/polys/tests/test_rootoftools.py
+++ b/sympy/polys/tests/test_rootoftools.py
@@ -276,6 +276,18 @@ def test_CRootOf_evalf():
         assert i == i.func(*i.args)
 
 
+def test_issue_24978():
+    # Irreducible poly with negative leading coeff is normalized
+    # (factor of -1 is extracted), before being stored as CRootOf.poly.
+    f = -x**2 + 2
+    r = CRootOf(f, 0)
+    assert r.poly.as_expr() == x**2 - 2
+    # An action that prompts calculation of an interval puts r.poly in
+    # the cache.
+    r.n()
+    assert r.poly in rootoftools._reals_cache
+
+
 def test_CRootOf_evalf_caching_bug():
     r = rootof(x**5 - 5*x + 12, 1)
     r.n()