r""" Functions in ``polys.numberfields.subfield`` solve the "Subfield Problem" and allied problems, for algebraic number fields. Following Cohen (see [Cohen93]_ Section 4.5), we can define the main problem as follows: * **Subfield Problem:** Given two number fields $\mathbb{Q}(\alpha)$, $\mathbb{Q}(\beta)$ via the minimal polynomials for their generators $\alpha$ and $\beta$, decide whether one field is isomorphic to a subfield of the other. From a solution to this problem flow solutions to the following problems as well: * **Primitive Element Problem:** Given several algebraic numbers $\alpha_1, \ldots, \alpha_m$, compute a single algebraic number $\theta$ such that $\mathbb{Q}(\alpha_1, \ldots, \alpha_m) = \mathbb{Q}(\theta)$. * **Field Isomorphism Problem:** Decide whether two number fields $\mathbb{Q}(\alpha)$, $\mathbb{Q}(\beta)$ are isomorphic. * **Field Membership Problem:** Given two algebraic numbers $\alpha$, $\beta$, decide whether $\alpha \in \mathbb{Q}(\beta)$, and if so write $\alpha = f(\beta)$ for some $f(x) \in \mathbb{Q}[x]$. """ from sympy.core.add import Add from sympy.core.numbers import AlgebraicNumber from sympy.core.singleton import S from sympy.core.symbol import Dummy from sympy.core.sympify import sympify from sympy.ntheory import sieve from sympy.polys.densetools import dup_eval from sympy.polys.domains import QQ from sympy.polys.numberfields.minpoly import _choose_factor, minimal_polynomial from sympy.polys.polyerrors import IsomorphismFailed from sympy.polys.polytools import Poly, PurePoly, factor_list from sympy.utilities import public from mpmath import MPContext def is_isomorphism_possible(a, b): """Necessary but not sufficient test for isomorphism. """ n = a.minpoly.degree() m = b.minpoly.degree() if m % n != 0: return False if n == m: return True da = a.minpoly.discriminant() db = b.minpoly.discriminant() i, k, half = 1, m//n, db//2 while True: p = sieve[i] P = p**k if P > half: break if ((da % p) % 2) and not (db % P): return False i += 1 return True def field_isomorphism_pslq(a, b): """Construct field isomorphism using PSLQ algorithm. """ if not a.root.is_real or not b.root.is_real: raise NotImplementedError("PSLQ doesn't support complex coefficients") f = a.minpoly g = b.minpoly.replace(f.gen) n, m, prev = 100, b.minpoly.degree(), None ctx = MPContext() for i in range(1, 5): A = a.root.evalf(n) B = b.root.evalf(n) basis = [1, B] + [ B**i for i in range(2, m) ] + [-A] ctx.dps = n coeffs = ctx.pslq(basis, maxcoeff=10**10, maxsteps=1000) if coeffs is None: # PSLQ can't find an integer linear combination. Give up. break if coeffs != prev: prev = coeffs else: # Increasing precision didn't produce anything new. Give up. break # We have # c0 + c1*B + c2*B^2 + ... + cm-1*B^(m-1) - cm*A ~ 0. # So bring cm*A to the other side, and divide through by cm, # for an approximate representation of A as a polynomial in B. # (We know cm != 0 since `b.minpoly` is irreducible.) coeffs = [S(c)/coeffs[-1] for c in coeffs[:-1]] # Throw away leading zeros. while not coeffs[-1]: coeffs.pop() coeffs = list(reversed(coeffs)) h = Poly(coeffs, f.gen, domain='QQ') # We only have A ~ h(B). We must check whether the relation is exact. if f.compose(h).rem(g).is_zero: # Now we know that h(b) is in fact equal to _some conjugate of_ a. # But from the very precise approximation A ~ h(B) we can assume # the conjugate is a itself. return coeffs else: n *= 2 return None def field_isomorphism_factor(a, b): """Construct field isomorphism via factorization. """ _, factors = factor_list(a.minpoly, extension=b) for f, _ in factors: if f.degree() == 1: # Any linear factor f(x) represents some conjugate of a in QQ(b). # We want to know whether this linear factor represents a itself. # Let f = x - c c = -f.rep.TC() # Write c as polynomial in b coeffs = c.to_sympy_list() d, terms = len(coeffs) - 1, [] for i, coeff in enumerate(coeffs): terms.append(coeff*b.root**(d - i)) r = Add(*terms) # Check whether we got the number a if a.minpoly.same_root(r, a): return coeffs # If none of the linear factors represented a in QQ(b), then in fact a is # not an element of QQ(b). return None @public def field_isomorphism(a, b, *, fast=True): r""" Find an embedding of one number field into another. Explanation =========== This function looks for an isomorphism from $\mathbb{Q}(a)$ onto some subfield of $\mathbb{Q}(b)$. Thus, it solves the Subfield Problem. Examples ======== >>> from sympy import sqrt, field_isomorphism, I >>> print(field_isomorphism(3, sqrt(2))) # doctest: +SKIP [3] >>> print(field_isomorphism( I*sqrt(3), I*sqrt(3)/2)) # doctest: +SKIP [2, 0] Parameters ========== a : :py:class:`~.Expr` Any expression representing an algebraic number. b : :py:class:`~.Expr` Any expression representing an algebraic number. fast : boolean, optional (default=True) If ``True``, we first attempt a potentially faster way of computing the isomorphism, falling back on a slower method if this fails. If ``False``, we go directly to the slower method, which is guaranteed to return a result. Returns ======= List of rational numbers, or None If $\mathbb{Q}(a)$ is not isomorphic to some subfield of $\mathbb{Q}(b)$, then return ``None``. Otherwise, return a list of rational numbers representing an element of $\mathbb{Q}(b)$ to which $a$ may be mapped, in order to define a monomorphism, i.e. an isomorphism from $\mathbb{Q}(a)$ to some subfield of $\mathbb{Q}(b)$. The elements of the list are the coefficients of falling powers of $b$. """ a, b = sympify(a), sympify(b) if not a.is_AlgebraicNumber: a = AlgebraicNumber(a) if not b.is_AlgebraicNumber: b = AlgebraicNumber(b) a = a.to_primitive_element() b = b.to_primitive_element() if a == b: return a.coeffs() n = a.minpoly.degree() m = b.minpoly.degree() if n == 1: return [a.root] if m % n != 0: return None if fast: try: result = field_isomorphism_pslq(a, b) if result is not None: return result except NotImplementedError: pass return field_isomorphism_factor(a, b) def _switch_domain(g, K): # An algebraic relation f(a, b) = 0 over Q can also be written # g(b) = 0 where g is in Q(a)[x] and h(a) = 0 where h is in Q(b)[x]. # This function transforms g into h where Q(b) = K. frep = g.rep.inject() hrep = frep.eject(K, front=True) return g.new(hrep, g.gens[0]) def _linsolve(p): # Compute root of linear polynomial. c, d = p.rep.rep return -d/c @public def primitive_element(extension, x=None, *, ex=False, polys=False): r""" Find a single generator for a number field given by several generators. Explanation =========== The basic problem is this: Given several algebraic numbers $\alpha_1, \alpha_2, \ldots, \alpha_n$, find a single algebraic number $\theta$ such that $\mathbb{Q}(\alpha_1, \alpha_2, \ldots, \alpha_n) = \mathbb{Q}(\theta)$. This function actually guarantees that $\theta$ will be a linear combination of the $\alpha_i$, with non-negative integer coefficients. Furthermore, if desired, this function will tell you how to express each $\alpha_i$ as a $\mathbb{Q}$-linear combination of the powers of $\theta$. Examples ======== >>> from sympy import primitive_element, sqrt, S, minpoly, simplify >>> from sympy.abc import x >>> f, lincomb, reps = primitive_element([sqrt(2), sqrt(3)], x, ex=True) Then ``lincomb`` tells us the primitive element as a linear combination of the given generators ``sqrt(2)`` and ``sqrt(3)``. >>> print(lincomb) [1, 1] This means the primtiive element is $\sqrt{2} + \sqrt{3}$. Meanwhile ``f`` is the minimal polynomial for this primitive element. >>> print(f) x**4 - 10*x**2 + 1 >>> print(minpoly(sqrt(2) + sqrt(3), x)) x**4 - 10*x**2 + 1 Finally, ``reps`` (which was returned only because we set keyword arg ``ex=True``) tells us how to recover each of the generators $\sqrt{2}$ and $\sqrt{3}$ as $\mathbb{Q}$-linear combinations of the powers of the primitive element $\sqrt{2} + \sqrt{3}$. >>> print([S(r) for r in reps[0]]) [1/2, 0, -9/2, 0] >>> theta = sqrt(2) + sqrt(3) >>> print(simplify(theta**3/2 - 9*theta/2)) sqrt(2) >>> print([S(r) for r in reps[1]]) [-1/2, 0, 11/2, 0] >>> print(simplify(-theta**3/2 + 11*theta/2)) sqrt(3) Parameters ========== extension : list of :py:class:`~.Expr` Each expression must represent an algebraic number $\alpha_i$. x : :py:class:`~.Symbol`, optional (default=None) The desired symbol to appear in the computed minimal polynomial for the primitive element $\theta$. If ``None``, we use a dummy symbol. ex : boolean, optional (default=False) If and only if ``True``, compute the representation of each $\alpha_i$ as a $\mathbb{Q}$-linear combination over the powers of $\theta$. polys : boolean, optional (default=False) If ``True``, return the minimal polynomial as a :py:class:`~.Poly`. Otherwise return it as an :py:class:`~.Expr`. Returns ======= Pair (f, coeffs) or triple (f, coeffs, reps), where: ``f`` is the minimal polynomial for the primitive element. ``coeffs`` gives the primitive element as a linear combination of the given generators. ``reps`` is present if and only if argument ``ex=True`` was passed, and is a list of lists of rational numbers. Each list gives the coefficients of falling powers of the primitive element, to recover one of the original, given generators. """ if not extension: raise ValueError("Cannot compute primitive element for empty extension") if x is not None: x, cls = sympify(x), Poly else: x, cls = Dummy('x'), PurePoly if not ex: gen, coeffs = extension[0], [1] g = minimal_polynomial(gen, x, polys=True) for ext in extension[1:]: _, factors = factor_list(g, extension=ext) g = _choose_factor(factors, x, gen) s, _, g = g.sqf_norm() gen += s*ext coeffs.append(s) if not polys: return g.as_expr(), coeffs else: return cls(g), coeffs gen, coeffs = extension[0], [1] f = minimal_polynomial(gen, x, polys=True) K = QQ.algebraic_field((f, gen)) # incrementally constructed field reps = [K.unit] # representations of extension elements in K for ext in extension[1:]: p = minimal_polynomial(ext, x, polys=True) L = QQ.algebraic_field((p, ext)) _, factors = factor_list(f, domain=L) f = _choose_factor(factors, x, gen) s, g, f = f.sqf_norm() gen += s*ext coeffs.append(s) K = QQ.algebraic_field((f, gen)) h = _switch_domain(g, K) erep = _linsolve(h.gcd(p)) # ext as element of K ogen = K.unit - s*erep # old gen as element of K reps = [dup_eval(_.rep, ogen, K) for _ in reps] + [erep] H = [_.rep for _ in reps] if not polys: return f.as_expr(), coeffs, H else: return f, coeffs, H @public def to_number_field(extension, theta=None, *, gen=None): r""" Express one algebraic number in the field generated by another. Explanation =========== Given two algebraic numbers $\eta, \theta$, this function either expresses $\eta$ as an element of $\mathbb{Q}(\theta)$, or else raises an exception if $\eta \not\in \mathbb{Q}(\theta)$. This function is essentially just a convenience, utilizing :py:func:`~.field_isomorphism` (our solution of the Subfield Problem) to solve this, the Field Membership Problem. As an additional convenience, this function allows you to pass a list of algebraic numbers $\alpha_1, \alpha_2, \ldots, \alpha_n$ instead of $\eta$. It then computes $\eta$ for you, as a solution of the Primitive Element Problem, using :py:func:`~.primitive_element` on the list of $\alpha_i$. Examples ======== >>> from sympy import sqrt, to_number_field >>> eta = sqrt(2) >>> theta = sqrt(2) + sqrt(3) >>> a = to_number_field(eta, theta) >>> print(type(a)) >>> a.root sqrt(2) + sqrt(3) >>> print(a) sqrt(2) >>> a.coeffs() [1/2, 0, -9/2, 0] We get an :py:class:`~.AlgebraicNumber`, whose ``.root`` is $\theta$, whose value is $\eta$, and whose ``.coeffs()`` show how to write $\eta$ as a $\mathbb{Q}$-linear combination in falling powers of $\theta$. Parameters ========== extension : :py:class:`~.Expr` or list of :py:class:`~.Expr` Either the algebraic number that is to be expressed in the other field, or else a list of algebraic numbers, a primitive element for which is to be expressed in the other field. theta : :py:class:`~.Expr`, None, optional (default=None) If an :py:class:`~.Expr` representing an algebraic number, behavior is as described under **Explanation**. If ``None``, then this function reduces to a shorthand for calling :py:func:`~.primitive_element` on ``extension`` and turning the computed primitive element into an :py:class:`~.AlgebraicNumber`. gen : :py:class:`~.Symbol`, None, optional (default=None) If provided, this will be used as the generator symbol for the returned :py:class:`~.AlgebraicNumber`. Returns ======= AlgebraicNumber Belonging to $\mathbb{Q}(\theta)$ and equaling $\eta$. Raises ====== IsomorphismFailed If $\eta \not\in \mathbb{Q}(\theta)$. See Also ======== field_isomorphism primitive_element """ if hasattr(extension, '__iter__'): extension = list(extension) else: extension = [extension] if len(extension) == 1 and isinstance(extension[0], tuple): return AlgebraicNumber(extension[0]) minpoly, coeffs = primitive_element(extension, gen, polys=True) root = sum([ coeff*ext for coeff, ext in zip(coeffs, extension) ]) if theta is None: return AlgebraicNumber((minpoly, root)) else: theta = sympify(theta) if not theta.is_AlgebraicNumber: theta = AlgebraicNumber(theta, gen=gen) coeffs = field_isomorphism(root, theta) if coeffs is not None: return AlgebraicNumber(theta, coeffs) else: raise IsomorphismFailed( "%s is not in a subfield of %s" % (root, theta.root))