Repository: sympy/sympy
Pull request: Add convenience methods for constructing `AlgebraicField`
<!-- Your title above should be a short description of what
was changed. Do not include the issue number in the title. -->

#### References to other Issues or PRs
<!-- If this pull request fixes an issue, write "Fixes #NNNN" in that exact
format, e.g. "Fixes #1234" (see
https://tinyurl.com/auto-closing for more information). Also, please
write a comment on that issue linking back to this pull request once it is
open. -->

Fixes #22954 


#### Brief description of what is fixed or changed

Adds a couple of convenience methods for constructing `AlgebraicField`.

Serves users who don't care about the complex embedding, by choosing an embedding for them.

In particular, makes constructing a cyclotomic field much easier.


#### Other comments


#### 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.

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 -->
* polys
  * Add convenience constructors for `AlgebraicField`
<!-- END RELEASE NOTES -->


Problem statement:
polys/numberfields: Convenience constructors for `AlgebraicField`
I'd like to propose a pair of new constructor methods, to support a couple of common use cases.


## Use Case 1

* You want a number field `Q(a)`,
* AND you have the minimal polynomial `f(x)` for `a`,
* AND you don't care _which_ root of `f` `a` is. (E.g. it's a Galois field, so it doesn't matter.),
* AND, OPTIONALLY you want an `alias` for `a` so that field elements print nicely.


## Use Case 2

* You want the `n`th cyclotomic field.
* You probably want the primitive element to be aliased either `"zeta"` or `f"zeta{n}"`.



## The Current Situation

For Use Case 1, you have to do this:
```python
alpha = AlgebraicNumber(CRootOf(f, -1), alias="alpha")
K = QQ.algebraic_field(alpha)

```
which has several problems:

* It's too long.
* You shouldn't have to import `CRootOf`.
* You shouldn't have to choose an index for `CRootOf`.


Use Case 2 is similar, and you also have to import `cyclotomic_poly`:

```python
zeta = AlgebraicNumber(CRootOf(cyclotomic_poly(19), -1), alias="zeta")
K = QQ.algebraic_field(zeta)
```



## Proposal

Alongside the `QQ.algebraic_field()` method (which takes one arg, being an `Expr`) I would propose two additional methods:

```python
    def alg_field_from_poly(self, poly, alias=None, root_index=-1):
        alpha = AlgebraicNumber(CRootOf(poly, root_index), alias=alias)
        return self.algebraic_field(alpha)

    def cyclotomic_field(self, n, alias="zeta", ss=False, gen=None, root_index=-1):
        if ss:
            alias += str(n)
        return self.alg_field_from_poly(cyclotomic_poly(n, gen), alias=alias, root_index=root_index)

```

Then the above examples become

```python
K = QQ.alg_field_from_poly(f, "alpha")
```

and

```python
K = QQ.cyclotomic_field(19)
```

## Rationale

For a "canonical" default root, `-1` is the best index to pass to `CRootOf`. In common cases like quadratic or cyclotomic fields, this will select the root you tend to think of: the positive square root, or the square root with positive imaginary part, or `exp(2*I*pi/n)` for the `n`th cyclotomic field.

Hints from issue discussion:
How would `to_sympy` or `from_sympy` work if we don't know which root is the generator?
We still do. These methods choose the root for you.
I think this seems reasonable.

Commit message: Simplify some tests.

These are unit and doctests where we construct an `AlgebraicField`.
We now make simpler constructions, using the new convenience constructor methods.

Patch to review:
diff --git a/sympy/polys/domains/domain.py b/sympy/polys/domains/domain.py
index 6228affacbfa..5a5ca468ccb7 100644
--- a/sympy/polys/domains/domain.py
+++ b/sympy/polys/domains/domain.py
@@ -3,6 +3,7 @@
 
 from typing import Any, Optional, Type
 
+from sympy.core.numbers import AlgebraicNumber
 from sympy.core import Basic, sympify
 from sympy.core.sorting import default_sort_key, ordered
 from sympy.external.gmpy import HAS_GMPY
@@ -891,6 +892,90 @@ def algebraic_field(self, *extension):
         r"""Returns an algebraic field, i.e. `K(\alpha, \ldots)`. """
         raise DomainError("Cannot create algebraic field over %s" % self)
 
+    def alg_field_from_poly(self, poly, alias=None, root_index=-1):
+        r"""
+        Convenience method to construct an algebraic extension on a root of a
+        polynomial, chosen by root index.
+
+        Parameters
+        ==========
+
+        poly : :py:class:`~.Poly`
+            The polynomial whose root generates the extension.
+        alias : str, optional (default=None)
+            Symbol name for the generator of the extension.
+            E.g. "alpha" or "theta".
+        root_index : int, optional (default=-1)
+            Specifies which root of the polynomial is desired. The ordering is
+            as defined by the :py:class:`~.ComplexRootOf` class. The default of
+            ``-1`` selects the most natural choice in the common cases of
+            quadratic and cyclotomic fields (the square root on the positive
+            real or imaginary axis, resp. $\mathrm{e}^{2\pi i/n}$).
+
+        Examples
+        ========
+
+        >>> from sympy import QQ, Poly
+        >>> from sympy.abc import x
+        >>> f = Poly(x**2 - 2)
+        >>> K = QQ.alg_field_from_poly(f)
+        >>> K.ext.minpoly == f
+        True
+        >>> g = Poly(8*x**3 - 6*x - 1)
+        >>> L = QQ.alg_field_from_poly(g, alias="alpha")
+        >>> L.ext.minpoly == g
+        True
+        >>> L.to_sympy(L([1, 1, 1]))
+        alpha**2 + alpha + 1
+
+        """
+        from sympy.polys.rootoftools import CRootOf
+        root = CRootOf(poly, root_index)
+        alpha = AlgebraicNumber(root, alias=alias)
+        return self.algebraic_field(alpha)
+
+    def cyclotomic_field(self, n, alias="zeta", ss=False, gen=None, root_index=-1):
+        r"""
+        Convenience method to construct a cyclotomic field.
+
+        Parameters
+        ==========
+
+        n : int
+            Construct the nth cyclotomic field.
+        alias : str, optional (default="zeta")
+            Symbol name for the generator.
+        ss : boolean, optional (default=False)
+            If True, append *n* as a subscript on the alias string.
+        gen : :py:class:`~.Symbol`, optional (default=None)
+            Desired variable for the cyclotomic polynomial that defines the
+            field. If ``None``, a dummy variable will be used.
+        root_index : int, optional (default=-1)
+            Specifies which root of the polynomial is desired. The ordering is
+            as defined by the :py:class:`~.ComplexRootOf` class. The default of
+            ``-1`` selects the root $\mathrm{e}^{2\pi i/n}$.
+
+        Examples
+        ========
+
+        >>> from sympy import QQ, latex
+        >>> K = QQ.cyclotomic_field(5)
+        >>> K.to_sympy(K([-1, 1]))
+        1 - zeta
+        >>> L = QQ.cyclotomic_field(7, ss=True)
+        >>> a = L.to_sympy(L([-1, 1]))
+        >>> print(a)
+        1 - zeta7
+        >>> print(latex(a))
+        1 - \zeta_{7}
+
+        """
+        from sympy.polys.specialpolys import cyclotomic_poly
+        if ss:
+            alias += str(n)
+        return self.alg_field_from_poly(cyclotomic_poly(n, gen), alias=alias,
+                                        root_index=root_index)
+
     def inject(self, *symbols):
         """Inject generators into this domain. """
         raise NotImplementedError
diff --git a/sympy/polys/domains/tests/test_domains.py b/sympy/polys/domains/tests/test_domains.py
index 40b1cb059946..52a83be06d22 100644
--- a/sympy/polys/domains/tests/test_domains.py
+++ b/sympy/polys/domains/tests/test_domains.py
@@ -17,7 +17,9 @@
 from sympy.polys.domains.polynomialring import PolynomialRing
 from sympy.polys.domains.realfield import RealField
 
+from sympy.polys.numberfields.subfield import field_isomorphism
 from sympy.polys.rings import ring
+from sympy.polys.specialpolys import cyclotomic_poly
 from sympy.polys.fields import field
 
 from sympy.polys.agca.extensions import FiniteExtension
@@ -725,6 +727,38 @@ def test_Domain__algebraic_field():
     assert alg.dom == QQ
 
 
+def test_Domain_alg_field_from_poly():
+    f = Poly(x**2 - 2)
+    g = Poly(x**2 - 3)
+    h = Poly(x**4 - 10*x**2 + 1)
+
+    alg = ZZ.alg_field_from_poly(f)
+    assert alg.ext.minpoly == f
+    assert alg.dom == QQ
+
+    alg = QQ.alg_field_from_poly(f)
+    assert alg.ext.minpoly == f
+    assert alg.dom == QQ
+
+    alg = alg.alg_field_from_poly(g)
+    assert alg.ext.minpoly == h
+    assert alg.dom == QQ
+
+
+def test_Domain_cyclotomic_field():
+    K = ZZ.cyclotomic_field(12)
+    assert K.ext.minpoly == Poly(cyclotomic_poly(12))
+    assert K.dom == QQ
+
+    F = QQ.cyclotomic_field(3)
+    assert F.ext.minpoly == Poly(cyclotomic_poly(3))
+    assert F.dom == QQ
+
+    E = F.cyclotomic_field(4)
+    assert field_isomorphism(E.ext, K.ext) is not None
+    assert E.dom == QQ
+
+
 def test_PolynomialRing_from_FractionField():
     F, x,y = field("x,y", ZZ)
     R, X,Y = ring("x,y", ZZ)
diff --git a/sympy/polys/numberfields/basis.py b/sympy/polys/numberfields/basis.py
index 9f414d6b8c19..9cb4b0e0aab4 100644
--- a/sympy/polys/numberfields/basis.py
+++ b/sympy/polys/numberfields/basis.py
@@ -115,15 +115,15 @@ def round_two(T, radicals=None):
     Working through an AlgebraicField:
 
     >>> from sympy import Poly, QQ
-    >>> from sympy.abc import x, theta
+    >>> from sympy.abc import x
     >>> T = Poly(x ** 3 + x ** 2 - 2 * x + 8)
-    >>> K = QQ.algebraic_field((T, theta))
+    >>> K = QQ.alg_field_from_poly(T, "theta")
     >>> print(K.maximal_order())
     Submodule[[2, 0, 0], [0, 2, 0], [0, 1, 1]]/2
     >>> print(K.discriminant())
     -503
     >>> print(K.integral_basis(fmt='sympy'))
-    [1, theta, theta**2/2 + theta/2]
+    [1, theta, theta/2 + theta**2/2]
 
     Calling directly:
 
diff --git a/sympy/polys/numberfields/primes.py b/sympy/polys/numberfields/primes.py
index 38b5dec0b214..dd3f6145cbfc 100644
--- a/sympy/polys/numberfields/primes.py
+++ b/sympy/polys/numberfields/primes.py
@@ -385,11 +385,8 @@ def prime_valuation(I, P):
     ========
 
     >>> from sympy import QQ
-    >>> from sympy.abc import theta
-    >>> from sympy.polys import cyclotomic_poly
     >>> from sympy.polys.numberfields import prime_valuation
-    >>> T = cyclotomic_poly(5)
-    >>> K = QQ.algebraic_field((T, theta))
+    >>> K = QQ.cyclotomic_field(5)
     >>> P = K.primes_above(5)
     >>> ZK = K.maximal_order()
     >>> print(prime_valuation(25*ZK, P[0]))
diff --git a/sympy/polys/numberfields/tests/test_basis.py b/sympy/polys/numberfields/tests/test_basis.py
index dfa326cc51c3..c7ff79b7d5f6 100644
--- a/sympy/polys/numberfields/tests/test_basis.py
+++ b/sympy/polys/numberfields/tests/test_basis.py
@@ -1,4 +1,4 @@
-from sympy.abc import theta, x
+from sympy.abc import x
 from sympy.core import S
 from sympy.core.numbers import AlgebraicNumber
 from sympy.functions.elementary.miscellaneous import sqrt
@@ -61,7 +61,7 @@ def test_round_two():
         (x**3 + 15 * x**2 - 9 * x + 13, DM([((1, 6), (1, 3), (1, 6)), (0, 1, 0), (0, 0, 1)], QQ).transpose(), -5292),
     )
     for f, B_exp, d_exp in cases:
-        K = QQ.algebraic_field((f, theta))
+        K = QQ.alg_field_from_poly(f)
         B = K.maximal_order().QQ_matrix
         d = K.discriminant()
         assert d == d_exp
diff --git a/sympy/polys/numberfields/tests/test_primes.py b/sympy/polys/numberfields/tests/test_primes.py
index a648f50e7308..09e31e012918 100644
--- a/sympy/polys/numberfields/tests/test_primes.py
+++ b/sympy/polys/numberfields/tests/test_primes.py
@@ -163,7 +163,7 @@ def test_decomp_6():
 def test_decomp_7():
     # Try working through an AlgebraicField
     T = Poly(x ** 3 + x ** 2 - 2 * x + 8)
-    K = QQ.algebraic_field((T, theta))
+    K = QQ.alg_field_from_poly(T)
     p = 2
     P = K.primes_above(p)
     ZK = K.maximal_order()


Code review comments:
diff_hunk: @@ -891,6 +892,90 @@ def algebraic_field(self, *extension):
         r"""Returns an algebraic field, i.e. `K(\alpha, \ldots)`. """
         raise DomainError("Cannot create algebraic field over %s" % self)
 
+    def alg_field_from_poly(self, poly, alias=None, root_index=-1):
+        r"""
+        Convenience method to construct an algebraic extension on a root of a
+        polynomial, chosen by root index.
+
+        Parameters
+        ==========
+
+        poly : :py:class:`~.Poly`
+            The polynomial whose root generates the extension.
+        alias : str, optional (default=None)
+            Symbol name for the generator of the extension.
+            E.g. "alpha" or "theta".
+        root_index : int, optional (default=-1)
+            Specifies which root of the polynomial is desired. The ordering is
+            as defined by the :py:class:`~.ComplexRootOf` class. The default of
+            ``-1`` selects the most natural choice in the common cases of
+            quadratic and cyclotomic fields (the square root on the positive
+            real or imaginary axis, resp. $\mathrm{e}^{2\pi i/n}$).
+
+        Examples
+        ========
+
+        >>> from sympy import QQ, Poly
+        >>> from sympy.abc import x
+        >>> f = Poly(x**2 - 2)
+        >>> K = QQ.alg_field_from_poly(f)
+        >>> K.ext.minpoly == f
+        True
+        >>> g = Poly(8*x**3 - 6*x - 1)
+        >>> L = QQ.alg_field_from_poly(g, alias="alpha")
+        >>> L.ext.minpoly == g
+        True
+        >>> L.to_sympy(L([1, 1, 1]))
+        alpha**2 + alpha + 1
+
+        """
+        from sympy.polys.rootoftools import CRootOf
+        root = CRootOf(poly, root_index)
+        alpha = AlgebraicNumber(root, alias=alias)
+        return self.algebraic_field(alpha)
+
+    def cyclotomic_field(self, n, alias="zeta", ss=False, gen=None, root_index=-1):
path: sympy/polys/domains/domain.py
text: @author:
Checking this over, I realized the order of the args `ss` and `alias` should be swapped. It will be more common to want to activate the subscript than to want to use a symbol other than `zeta`.

I'll make this change and (if still no objections) will merge soon.