Repository: sagemath/sage

Issue:
Compute the characteristic polynomial of the Frobenius endomorphism of a Drinfeld module
### Is there an existing issue for this?

- [X] I have searched the existing issues for a bug report that matches the one I want to file, without success.

### Problem Description

The Frobenius is a distinguished element of the endomorphism ring of a Drinfeld module, and its characteristic polynomial is considered an important invariant attached to it. There is currently a pull request [#35026](https://github.com/sagemath/sage/pull/35026) open for the implementation of Drinfeld modules; however the implementation currently only allows for the computation of the characteristic polynomial for Drinfeld modules of rank 2. 

I propose extending this implementation to allow for computing the characteristic polynomial of the Frobenius endomorphism on Drinfeld modules of any rank.



### Proposed Solution

There are algorithms that allow one to compute the characteristic polynomial in any rank. I propose implementing one of these algorithms as part of the DrinfeldModule class or a related class. A related solution could be to allow the computation of characteristic polynomials of any morphism acting on a Drinfeld module.

### Alternatives Considered

There are not really any alternatives other than the ones mentioned in proposed solution.

### Additional Information

_No response_


Discussion:
Amazing, thank you @ymusleh!

As discussed at SageDays 117, I suggest creating multiples (private) methods to compute the characteristic polynomial of the Frobenius endomorphism, one for each algorithm. The user chooses which algorithm with the flag `algorithm` of method `frobenius_charpoly`. Example:

```
sage: chi = phi.frobenius_charpoly(algorithm='crystalline')
```

I think each method should be named `_frobenius_charpoly_algorithm` (e.g. `_frobenius_charpoly_crystalline`). We can figure out a smart way to code this.

In the long term, this code might be moved to `DrinfeldModuleMorphism` (this would not mean a deprecation because we would keep `DrinfeldModule` method `frobenius_charpoly`).

Resolving patch:
diff --git a/src/sage/rings/function_field/drinfeld_modules/finite_drinfeld_module.py b/src/sage/rings/function_field/drinfeld_modules/finite_drinfeld_module.py
--- a/src/sage/rings/function_field/drinfeld_modules/finite_drinfeld_module.py
+++ b/src/sage/rings/function_field/drinfeld_modules/finite_drinfeld_module.py
@@ -10,6 +10,7 @@
 AUTHORS:
 
 - Antoine Leudière (2022-04)
+- Yossef Musleh (2023-02): added characteristic polynomial methods
 """
 
 # *****************************************************************************
@@ -22,10 +23,15 @@
 #                   http://www.gnu.org/licenses/
 # *****************************************************************************
 
+from sage.functions.log import logb
+from sage.functions.other import ceil, sqrt
 from sage.matrix.constructor import Matrix
+from sage.matrix.matrix_space import MatrixSpace
+from sage.matrix.special import companion_matrix
+from sage.misc.misc_c import prod
 from sage.modules.free_module_element import vector
-from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
 from sage.rings.function_field.drinfeld_modules.drinfeld_module import DrinfeldModule
+from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
 
 
 class DrinfeldModule_finite(DrinfeldModule):
@@ -151,8 +157,76 @@ def __init__(self, gen, category):
         # added one to ensure that DrinfeldModule_finite would always
         # have _frobenius_norm and _frobenius_trace attributes.
         super().__init__(gen, category)
+        self._base_degree_over_constants = self.base_over_constants_field().degree(self._Fq)
         self._frobenius_norm = None
         self._frobenius_trace = None
+        self._frobenius_charpoly = None
+
+    def _frobenius_crystalline_matrix(self):
+        r"""
+        Return the matrix representing the Frobenius endomorphism on the
+        crystalline cohomology of the Drinfeld module. This is done up to
+        precision [K:Fq] + 1, which is enough to ensure the characteristic
+        polynomial can be recovered.
+
+        For the formal construction of the crystalline cohomology, see
+        [Ang1997]_. It is a free module of rank r over the ring of Witt
+        vectors in `T - \mathfrak{p}`.
+
+        EXAMPLES::
+
+            sage: Fq = GF(25)
+            sage: A.<T> = Fq[]
+            sage: K.<z12> = Fq.extension(6)
+            sage: p_root = 2*z12^11 + 2*z12^10 + z12^9 + 3*z12^8 + z12^7 + 2*z12^5 + 2*z12^4 + 3*z12^3 + z12^2 + 2*z12
+            sage: phi = DrinfeldModule(A, [p_root, z12^3, z12^5])
+            sage: phi._frobenius_crystalline_matrix()
+            [...((z2 + 3) + z12 + (4*z2 + 1)*z12^2 + (z2 + 4)*z12^3 + (z2 + 4)*z12^4 + (2*z2 + 3)*z12^5)*T^3 + (2*z2 + 2*z2*z12 + (2*z2 + 3)*z12^2 + ... + (3*z2 + 4)*z12^3 + (2*z2 + 3)*z12^4 + 3*z2*z12^5]
+
+        ALGORITHM:
+
+        Compute the action of the Frobenius on an explicit basis for the
+        cohomology using a recurrence relation, and return the resulting
+        matrix.
+        """
+        Fq = self._Fq
+        K = self.base_over_constants_field()
+        A = self.function_ring()
+        char, q = Fq.characteristic(), Fq.cardinality()
+        qdeg = logb(q, char)
+        r, n = self.rank(), self._base_degree_over_constants
+        nstar = ceil(sqrt(n))
+        nquo, nrem = divmod(n, nstar)
+        drin_coeffs = self.coefficients(sparse=False)
+        poly_K = PolynomialRing(K, name=str(A.gen()))
+        matrix_poly_K = MatrixSpace(poly_K, r, r)
+        mu_coeffs = ((poly_K.gen() - drin_coeffs[0])**(n+1)) \
+                    .coefficients(sparse=False)
+
+        def companion(order):
+            # + [1] is required to satisfy formatting for companion matrix
+            M = matrix_poly_K(companion_matrix([(drin_coeffs[i]/drin_coeffs[r])
+                               .frobenius(qdeg*order)
+                               for i in range(r)] + [1], format='top'))
+            M[0, r-1] += poly_K.gen() / drin_coeffs[r].frobenius(qdeg*order)
+            return M
+
+        companion_initial = prod([companion(i) for i in range(nrem, 0, -1)])
+        companion_step = prod([companion(i)
+                              for i in range(nstar + nrem, nrem, -1)])
+        reduced_companions = []
+        for k in range(nquo - 1, 0, -1):
+            M = Matrix(poly_K, r, r)
+            modulus = poly_K([c.frobenius(qdeg*(-k*nstar % n))
+                                                for c in mu_coeffs])
+            for i, row in enumerate(companion_step):
+                for j, entry in enumerate(row):
+                    reduction = entry % modulus
+                    M[i, j] = poly_K([c.frobenius(qdeg*(k*nstar))
+                                     for c in reduction
+                                              .coefficients(sparse=False)])
+            reduced_companions.append(M)
+        return (prod(reduced_companions) * companion_step * companion_initial)
 
     def frobenius_endomorphism(self):
         r"""
@@ -183,46 +257,62 @@ def frobenius_endomorphism(self):
         deg = self.base_over_constants_field().degree_over()
         return self._Hom_(self, category=self.category())(t**deg)
 
-    def frobenius_charpoly(self, var='X'):
+    def frobenius_charpoly(self, var='X', algorithm='crystalline'):
         r"""
         Return the characteristic polynomial of the Frobenius
-        endomorphism if the rank is two. Raise a NotImplementedError
-        otherwise.
+        endomorphism.
 
         Let `\mathbb{F}_q` be the base field of the function ring. The
-        *characteristic polynomial `\chi` of the Frobenius endomorphism*
+        *characteristic polynomial* `\chi` *of the Frobenius endomorphism*
         is defined in [Gek1991]_. An important feature of this
-        polynomial is that it is a monic univariate polynomial with
-        coefficients in the function ring. As in our case the function
+        polynomial is that it is monic, univariate, and has coefficients
+        in the function ring. As in our case the function
         ring is a univariate polynomial ring, it is customary to see the
         characteristic polynomial of the Frobenius endomorphism as a
         bivariate polynomial.
 
-        Let `\chi = X^2 - A(T)X + B(T)` be the characteristic polynomial
-        of the Frobenius endomorphism, and let `t^n` be the Ore polynomial
-        that defines the Frobenius endomorphism of `\phi`; by
-        definition, `n` is the degree over of the base field over
-        `\mathbb{F}_q`. We have `\chi(t^n)(\phi(T)) = t^{2n} - \phi_A
-        t^n + \phi_B = 0`, with `\deg(A) \leq \frac{n}{2}` and `\deg(B)
-        = n`.
+        Let `\chi = X^r + \sum_{i=0}^{r-1} A_{i}(T)X^{i}` be the
+        characteristic polynomial of the Frobenius endomorphism, and
+        let `t^n` be the Ore polynomial that defines the Frobenius
+        endomorphism of `\phi`; by definition, `n` is the degree of `K`
+        over the base field `\mathbb{F}_q`. Then we have
+
+        .. MATH::
+
+            \chi(t^n)(\phi(T))
+            = t^{nr} + \sum_{i=1}^{r} \phi_{A_{i}}t^{n(i)}
+            = 0,
 
-        Note that the *Frobenius trace* is defined as `A(T)` and the
-        *Frobenius norm* is defined as `B(T)`.
+        with `\deg(A_i) \leq \frac{n(r-i)}{r}`.
+
+        Note that the *Frobenius trace* is defined as `A_{r-1}(T)` and the
+        *Frobenius norm* is defined as `A_0(T)`.
 
         INPUT:
 
         - ``var`` (default: ``'X'``) -- the name of the second variable
+        - ``algorithm`` (default: ``'crystalline'``) -- the algorithm
+          used to compute the characteristic polynomial
 
         EXAMPLES::
 
+            sage: Fq = GF(25)
+            sage: A.<T> = Fq[]
+            sage: K.<z12> = Fq.extension(6)
+            sage: p_root = 2*z12^11 + 2*z12^10 + z12^9 + 3*z12^8 + z12^7 + 2*z12^5 + 2*z12^4 + 3*z12^3 + z12^2 + 2*z12
+            sage: phi = DrinfeldModule(A, [p_root, z12^3, z12^5])
+            sage: phi.frobenius_charpoly()
+            X^2 + ((4*z2 + 4)*T^3 + (z2 + 3)*T^2 + 3*T + 2*z2 + 3)*X + 3*z2*T^6 + (4*z2 + 3)*T^5 + (4*z2 + 4)*T^4 + 2*T^3 + (3*z2 + 3)*T^2 + (z2 + 2)*T + 4*z2
+
+        ::
+
             sage: Fq = GF(343)
             sage: A.<T> = Fq[]
             sage: K.<z6> = Fq.extension(2)
             sage: phi = DrinfeldModule(A, [1, 0, z6])
             sage: chi = phi.frobenius_charpoly()
             sage: chi
-            X^2 + ((3*z3^2 + z3 + 4)*T + 4*z3^2 + 6*z3 + 3)*X
-             + (5*z3^2 + 2*z3)*T^2 + (4*z3^2 + 3*z3)*T + 5*z3^2 + 2*z3
+            X^2 + ((3*z3^2 + z3 + 4)*T + 4*z3^2 + 6*z3 + 3)*X + (5*z3^2 + 2*z3)*T^2 + (4*z3^2 + 3*z3)*T + 5*z3^2 + 2*z3
 
         ::
 
@@ -232,51 +322,165 @@ def frobenius_charpoly(self, var='X'):
 
         ::
 
-            sage: trace = phi.frobenius_trace()
-            sage: trace
-            (4*z3^2 + 6*z3 + 3)*T + 3*z3^2 + z3 + 4
-            sage: norm = phi.frobenius_norm()
-            sage: norm
-            (5*z3^2 + 2*z3)*T^2 + (4*z3^2 + 3*z3)*T + 5*z3^2 + 2*z3
+            sage: phi.frobenius_charpoly(algorithm="NotImplemented")
+            Traceback (most recent call last):
+            ...
+            NotImplementedError: algorithm "NotImplemented" not implemented
+
+        ALGORITHM:
+
+        By default, this method uses the so-called *crystalline*
+        algorithm which computes the characteristic polynomial of the
+        Frobenius acting on the crystalline cohomology of the Drinfeld
+        module. For further details, see [Ang1997]_.
+
+        The available options for 'algorithm' are:
+
+        - ``'crystalline'`` -- Computes the characteristic polynomial of the
+          Frobenius endomorphism on the crystalline cohomology of a Drinfeld
+          module.
+
+        - ``'motive'`` -- Based on computing the characteristic polynomial of
+          the Frobenius endomorphism on the motive of a Drinfeld module. This
+          instantiates the Frobenius as a morphism object and calls its
+          ``'characteristic_polynomial'`` method.
+        """
+        # Throw an error if the user asks for an unimplemented algorithm
+        # even if the char poly has already been computed
+        method_name = f'_frobenius_charpoly_{algorithm}'
+        if hasattr(self, method_name):
+            if self._frobenius_charpoly is not None:
+                return self._frobenius_charpoly
+            self._frobenius_charpoly = getattr(self, method_name)(var)
+            return self._frobenius_charpoly
+        raise NotImplementedError(f'algorithm \"{algorithm}\" not implemented')
+
+    def _frobenius_charpoly_crystalline(self, var):
+        r"""
+        Return the characteristic polynomial of the Frobenius
+        endomorphism using Crystalline cohomology.
+
+        The algorithm works for Drinfeld `\mathbb{F}_q[T]`-modules of
+        any rank.
+
+        This method is private and should not be directly called.
+        Instead, use :meth:`frobenius_charpoly` with the option
+        `algorithm='crystalline'`.
+
+        INPUT:
+
+        - ``var`` -- the name of the second variable
+
+        OUTPUT: a univariate polynomial with coefficients in the
+                function ring
+
+        EXAMPLES::
+
+            sage: Fq = GF(25)
+            sage: A.<T> = Fq[]
+            sage: K.<z12> = Fq.extension(6)
+            sage: p_root = 2*z12^11 + 2*z12^10 + z12^9 + 3*z12^8 + z12^7 + 2*z12^5 + 2*z12^4 + 3*z12^3 + z12^2 + 2*z12
+            sage: phi = DrinfeldModule(A, [p_root, z12^3, z12^5])
+            sage: phi.frobenius_charpoly(algorithm='crystalline') # indirect doctest
+            X^2 + ((4*z2 + 4)*T^3 + (z2 + 3)*T^2 + 3*T + 2*z2 + 3)*X + 3*z2*T^6 + (4*z2 + 3)*T^5 + (4*z2 + 4)*T^4 + 2*T^3 + (3*z2 + 3)*T^2 + (z2 + 2)*T + 4*z2
 
         ::
 
-            sage: n = 2  # Degree of the base field over Fq
-            sage: trace.degree() <= n/2
-            True
-            sage: norm.degree() == n
-            True
+            sage: Fq = GF(25)
+            sage: A.<T> = Fq[]
+            sage: K.<z> = Fq.extension(8)
+            sage: phi = DrinfeldModule(A, [z, 4, 1, z, z+1, 2, z+2, 1, 1, 3, 1])
+            sage: phi.frobenius_charpoly(algorithm='crystalline') # indirect doctest
+            X^10 + X^9 + (3*T + z2 + 1)*X^8 + (4*T^2 + z2*T + 2*z2 + 1)*X^7 + ... + (4*z2 + 4)*T^4 + 4*z2*T^2 + (z2 + 2)*T + z2
+
+        ::
+
+            sage: Fq = GF(27)
+            sage: A.<T> = Fq[]
+            sage: K.<z> = Fq.extension(10)
+            sage: phi = DrinfeldModule(A, [z, z^2 + z, 2, 1, z, z+1, 2, z+2, 0, 1, 1, z^2 + z])
+            sage: phi.frobenius_charpoly(algorithm='crystalline') # indirect doctest
+            X^11 + (z3^2 + 2*z3)*X^10 + ((z3 + 1)*T + z3)*X^9 + ((2*z3^2 + z3 + 2)*T^2 + ... + (2*z3^2 + 2*z3 + 2)*T + z3^2
 
         ALGORITHM:
 
-            We compute the Frobenius norm, and with it the Frobenius
-            trace. This gives the Frobenius characteristic polynomial.
-            See [MS2019]_, Section 4.
+        Compute the characteristic polynomial of the Frobenius endomorphism
+        acting on the crystalline cohomology of a Drinfeld module, which
+        is equal to that of the Frobenius endomorphism on the Drinfeld
+        module. A recurrence on elements of the cohomology allows us to
+        compute a matrix representation of the Frobenius endomorphism
+        efficiently using a companion matrix method. Based on the algorithm
+        of section 6.3 in [MS2023]_.
+        """
+        A = self.function_ring()
+        K = self.base_over_constants_field()
+        charpoly_K = self._frobenius_crystalline_matrix() \
+                         .charpoly(var).coefficients(sparse=False)
+
+        # The above line obtains the char poly with coefficients in K[T]
+        # This maps them into A = Fq[T]
+
+        coeffs_A = [A([x.in_base() for x in coeff]) for coeff in charpoly_K]
+        return PolynomialRing(A, name=var)(coeffs_A)
+
+    def _frobenius_charpoly_motive(self, var):
+        r"""
+        Return the characteristic polynomial of the Frobenius
+        endomorphism using Motivic cohomology.
+
+        The algorithm works for Drinfeld `\mathbb{F}_q[T]`-modules of
+        any rank.
+
+        This method is private and should not be directly called.
+        Instead, use :meth:`frobenius_charpoly` with the option
+        `algorithm='motive'`.
+
+        INPUT:
+
+        - ``var`` -- the name of the second variable
 
-            See docstrings of methods :meth:`frobenius_norm` and
-            :meth:`frobenius_trace` for further details on the
-            computation of the norm and of the trace.
+        OUTPUT: a univariate polynomial with coefficients in the
+                function ring
+
+        EXAMPLES::
+
+            sage: Fq = GF(25)
+            sage: A.<T> = Fq[]
+            sage: K.<z12> = Fq.extension(6)
+            sage: p_root = 2*z12^11 + 2*z12^10 + z12^9 + 3*z12^8 + z12^7 + 2*z12^5 + 2*z12^4 + 3*z12^3 + z12^2 + 2*z12
+            sage: phi = DrinfeldModule(A, [p_root, z12^3, z12^5])
+            sage: phi.frobenius_charpoly(algorithm='motive') # indirect doctest
+            X^2 + ((4*z2 + 4)*T^3 + (z2 + 3)*T^2 + 3*T + 2*z2 + 3)*X + 3*z2*T^6 + (4*z2 + 3)*T^5 + (4*z2 + 4)*T^4 + 2*T^3 + (3*z2 + 3)*T^2 + (z2 + 2)*T + 4*z2
+
+        ::
+
+            sage: Fq = GF(25)
+            sage: A.<T> = Fq[]
+            sage: K.<z> = Fq.extension(8)
+            sage: phi = DrinfeldModule(A, [z, 4, 1, z, z+1, 2, z+2, 1, 1, 3, 1])
+            sage: phi.frobenius_charpoly(algorithm='motive') # indirect doctest
+            X^10 + X^9 + (3*T + z2 + 1)*X^8 + (4*T^2 + z2*T + 2*z2 + 1)*X^7 + ... + (4*z2 + 4)*T^4 + 4*z2*T^2 + (z2 + 2)*T + z2
+
+        ::
+
+            sage: Fq = GF(27)
+            sage: A.<T> = Fq[]
+            sage: K.<z> = Fq.extension(10)
+            sage: phi = DrinfeldModule(A, [z, z^2 + z, 2, 1, z, z+1, 2, z+2, 0, 1, 1, z^2 + z])
+            sage: phi.frobenius_charpoly(algorithm='motive') # indirect doctest
+            X^11 + (z3^2 + 2*z3)*X^10 + ((z3 + 1)*T + z3)*X^9 + ((2*z3^2 + z3 + 2)*T^2 + ... + (2*z3^2 + 2*z3 + 2)*T + z3^2
         """
-        self._check_rank_two()
-        A = self._function_ring  # Fq[T]
-        S = PolynomialRing(A, name=var)  # Fq[T][X]
-        # Does not work when Fq is not a prime field...
-        # chi = self._gen.reduced_charpoly()
-        # return -chi(A.gen(), S.gen())
-        return S([self.frobenius_norm(), -self.frobenius_trace(), 1])
+        return self.frobenius_endomorphism().characteristic_polynomial(var)
 
     def frobenius_norm(self):
         r"""
-        Return Frobenius norm of the Drinfeld module, if the rank is
-        two, raise a NotImplementedError otherwise.
+        Return the Frobenius norm of the Drinfeld module.
 
-        Let `\mathbb{F}_q[T]` be the function ring, write `\chi = X^2 -
-        A(T)X + B(T) \in \mathbb{F}_q[T][X]` for the characteristic
-        polynomial of the Frobenius endomorphism. The *Frobenius norm*
-        is defined as the polynomial `B(T) \in \mathbb{F}_q[T]`.
-
-        Let `n` be the degree of the base field over `\mathbb{F}_q` Then the
-        Frobenius norm has degree `n`.
+        Let `C(X) = \sum_{i=0}^r a_iX^{i}` denote the characteristic
+        polynomial of the Frobenius endomorphism. The Frobenius norm
+        is `(-1)^r a_{0}`. This is an element of the regular function ring
+        and if `n` is the degree of the base field over `\mathbb{F}_q`,
+        then the Frobenius norm has degree `n`.
 
         EXAMPLES::
 
@@ -304,31 +508,24 @@ def frobenius_norm(self):
             The Frobenius norm is computed using the formula, by
             Gekeler, given in [MS2019]_, Section 4, Proposition 3.
         """
-        self._check_rank_two()
-        L = self._base.over(self._Fq)
-        # Notations from Schost-Musleh:
-        if self._frobenius_norm is None:
-            n = L.degree_over(self._Fq)
-            d = self.characteristic().degree()
-            m = n // d
-            delta = self._gen[2]
-            norm = L(delta).norm()
-            char = self.characteristic()
-            self._frobenius_norm = ((-1)**n) * (char**m) / norm
+        if self._frobenius_norm is not None:
+            return self._frobenius_norm
+        K = self.base_over_constants_field()
+        n = K.degree(self._Fq)
+        char = self.characteristic()
+        norm = K(self.coefficients()[-1]).norm()
+        self._frobenius_norm = ((-1)**n)*(char**(n/char.degree())) / norm
         return self._frobenius_norm
 
     def frobenius_trace(self):
         r"""
-        Return Frobenius norm of the Drinfeld module, if the rank is
-        two; raise a NotImplementedError otherwise.
+        Return the Frobenius trace of the Drinfeld module.
 
-        Let `\mathbb{F}_q[T]` be the function ring, write `\chi = T^2 -
-        A(X)T + B(X) \in \mathbb{F}_q[T][X]` for the characteristic
-        polynomial of the Frobenius endomorphism. The *Frobenius trace*
-        is defined as the polynomial `A(T) \in \mathbb{F}_q[T]`.
-
-        Let `n` be the degree over `\mathbb{F}_q` of the base codomain.
-        Then the Frobenius trace has degree at most `\frac{n}{2}`.
+        Let `C(X) = \sum_{i=0}^r a_iX^{i}` denote the characteristic
+        polynomial of the Frobenius endomorphism. The Frobenius trace
+        is `-a_{r-1}`. This is an element of the regular function ring
+        and if `n` is the degree of the base field over `\mathbb{F}_q`,
+        then the Frobenius trace has degree at most `\frac{n}{r}`.
 
         EXAMPLES::
 
@@ -353,24 +550,20 @@ def frobenius_trace(self):
 
         ALGORITHM:
 
-            Let `A(T)` denote the Frobenius trace and `B(T)` denote the
-            Frobenius norm. We begin by computing `B(T)`, see docstring
-            of method :meth:`frobenius_norm` for details. The
-            characteristic polynomial of the Frobenius yields `t^{2n} -
-            \phi_A t^n + \phi_B = 0`, where `t^n` is the Frobenius
-            endomorphism. As `\phi_B` is now known, we can compute
-            `\phi_A = (t^{2n} + \phi_B) / t^n`. We get `A(T)` by
-            inverting this quantity, using the method
-            :meth:`sage.rings.function_fields.drinfeld_module.drinfeld_module.DrinfeldModule.invert`,
-            see its docstring for details.
+            We extract the coefficient of `X^{r-1}` from the characteristic
+            polynomial if it has been previously computed, otherwise we compute
+            the trace of the matrix of the Frobenius acting on the crystalline
+            cohomology.
         """
-        self._check_rank_two()
-        # Notations from Schost-Musleh:
-        if self._frobenius_trace is None:
-            n = self._base.over(self._Fq).degree_over(self._Fq)
-            B = self.frobenius_norm()
-            t = self.ore_polring().gen()
-            self._frobenius_trace = self.invert(t**n + (self(B) // t**n))
+        K = self.base_over_constants_field()
+        A = self.function_ring()
+        if self._frobenius_trace is not None:
+            return self._frobenius_trace
+        if self._frobenius_charpoly is not None:
+            self._frobenius_trace = -self._frobenius_charpoly \
+                                    .coefficients(sparse=False)[-2]
+        self._frobenius_trace = self._frobenius_crystalline_matrix().trace()
+        self._frobenius_trace = A([x.in_base() for x in self._frobenius_trace])
         return self._frobenius_trace
 
     def invert(self, ore_pol):
@@ -487,6 +680,62 @@ def invert(self, ore_pol):
             pass
         raise ValueError('input must be in the image of the Drinfeld module')
 
+    def is_isogenous(self, psi):
+        r"""
+        Return ``True`` when ``self`` is isogenous to the other
+        Drinfeld module.
+
+        If the Drinfeld modules do not belong to the same category, an
+        exception is raised.
+
+        EXAMPLES::
+
+            sage: Fq = GF(2)
+            sage: A.<T> = Fq[]
+            sage: K.<z> = Fq.extension(3)
+            sage: psi = DrinfeldModule(A, [z, z + 1, z^2 + z + 1])
+            sage: phi = DrinfeldModule(A, [z, z^2 + z + 1, z^2 + z])
+            sage: phi.is_isogenous(psi)
+            True
+
+        ::
+
+            sage: chi = DrinfeldModule(A, [z, z + 1, z^2 + z])
+            sage: phi.is_isogenous(chi)
+            False
+
+        ::
+
+            sage: mu = DrinfeldModule(A, [z + 1, z^2 + z + 1, z^2 + z])
+            sage: phi.is_isogenous(mu)
+            Traceback (most recent call last):
+            ...
+            TypeError: Drinfeld modules are not in the same category
+
+        ::
+
+            sage: mu = 1
+            sage: phi.is_isogenous(mu)
+            Traceback (most recent call last):
+            ...
+            TypeError: input must be a Drinfeld module
+
+        ALGORITHM:
+
+        Two Drinfeld A-modules of equal characteristic are isogenous
+        if and only if:
+
+        - they have the same rank
+        - the characteristic polynomial of the Frobenius endomorphism
+          for both Drinfeld modules are equal.
+        """
+        if not isinstance(psi, DrinfeldModule):
+            raise TypeError("input must be a Drinfeld module")
+        if self.category() != psi.category():
+            raise TypeError("Drinfeld modules are not in the same category")
+        return self.rank() == psi.rank() \
+               and self.frobenius_charpoly() == psi.frobenius_charpoly()
+
     def is_supersingular(self):
         r"""
         Return ``True`` if this Drinfeld module is supersingular.


Test patch: