Drinfeld modules#

This module provides the class sage.rings.function_field.drinfeld_module.drinfeld_module.DrinfeldModule.

For finite Drinfeld modules and their theory of complex multiplication, see class sage.rings.function_field.drinfeld_module.finite_drinfeld_module.DrinfeldModule.

AUTHORS:

  • Antoine Leudière (2022-04)

  • Xavier Caruso (2022-06)

class sage.rings.function_field.drinfeld_modules.drinfeld_module.DrinfeldModule(gen, category)#

Bases: Parent, UniqueRepresentation

This class implements Drinfeld \(\mathbb{F}_q[T]\)-modules.

Let \(\mathbb{F}_q[T]\) be a polynomial ring with coefficients in a finite field \(\mathbb{F}_q\) and let \(K\) be a field. Fix a ring morphism \(\gamma: \mathbb{F}_q[T] \to K\); we say that \(K\) is an \(\mathbb{F}_q[T]\)-field. Let \(K\{\tau\}\) be the ring of Ore polynomials with coefficients in \(K\), whose multiplication is given by the rule \(\tau \lambda = \lambda^q \tau\) for any \(\lambda \in K\).

A Drinfeld \(\mathbb{F}_q[T]\)-module over the base \(\mathbb{F}_q[T]\)-field \(K\) is an \(\mathbb{F}_q\)-algebra morphism \(\phi: \mathbb{F}_q[T] \to K\{\tau\}\) such that \(\mathrm{Im}(\phi) \not\subset K\) and \(\phi\) agrees with \(\gamma\) on \(\mathbb{F}_q\).

For \(a\) in \(\mathbb{F}_q[T]\), \(\phi(a)\) is denoted \(\phi_a\).

The Drinfeld \(\mathbb{F}_q[T]\)-module \(\phi\) is uniquely determined by the image \(\phi_T\) of \(T\); this serves as input of the class.

The base morphism is the morphism \(\gamma: \mathbb{F}_q[T] \to K\). The monic polynomial that generates the kernel of \(\gamma\) is called the \(\mathbb{F}_q[T]\)-characteristic, or function-field characteristic, of the base field. We say that \(\mathbb{F}_q[T]\) is the function ring of \(\phi\); \(K\{\tau\}\) is the Ore polynomial ring. Further, the generator is \(\phi_T\) and the constant coefficient is the constant coefficient of \(\phi_T\).

A Drinfeld module is said to be finite if the field \(K\) is. Despite an emphasis on this case, the base field can be any extension of \(\mathbb{F}_q\):

sage: Fq = GF(25)
sage: A.<T> = Fq[]
sage: K.<z> = Fq.extension(6)
sage: phi = DrinfeldModule(A, [z, 4, 1])
sage: phi
Drinfeld module defined by T |--> t^2 + 4*t + z
sage: Fq = GF(49)
sage: A.<T> = Fq[]
sage: K = Frac(A)
sage: psi = DrinfeldModule(A, [K(T), T+1])
sage: psi
Drinfeld module defined by T |--> (T + 1)*t + T

Note

Finite Drinfeld modules are implemented in the class sage.rings.function_field.drinfeld_modules.finite_drinfeld_module.

Classical references on Drinfeld modules include [Gos1998], [Rosen2002], [VS06] and [Gek1991].

Note

Drinfeld modules are defined in a larger setting, in which the polynomial ring \(\mathbb{F}_q[T]\) is replaced by a more general function ring: the ring of functions in \(k\) that are regular outside \(\infty\), where \(k\) is a function field over \(\mathbb{F}_q\) with transcendence degree \(1\) and \(\infty\) is a fixed place of \(k\). This is out of the scope of this implementation.

INPUT:

  • function_ring – a univariate polynomial ring whose base field is a finite field

  • gen – the generator of the Drinfeld module; as a list of coefficients or an Ore polynomial

  • name (default: 't') – the name of the Ore polynomial ring generator

Construction

A Drinfeld module object is constructed by giving the function ring and the generator:

sage: Fq.<z2> = GF(3^2)
sage: A.<T> = Fq[]
sage: K.<z> = Fq.extension(6)
sage: phi = DrinfeldModule(A, [z, 1, 1])
sage: phi
Drinfeld module defined by T |--> t^2 + t + z

Note

Note that the definition of the base field is implicit; it is automatically defined as the compositum of all the parents of the coefficients.

The above Drinfeld module is finite; it can also be infinite:

sage: L = Frac(A)
sage: psi = DrinfeldModule(A, [L(T), 1, T^3 + T + 1])
sage: psi
Drinfeld module defined by T |--> (T^3 + T + 1)*t^2 + t + T
sage: phi.is_finite()
True
sage: psi.is_finite()
False

In those examples, we used a list of coefficients ([z, 1, 1]) to represent the generator \(\phi_T = z + t + t^2\). One can also use regular Ore polynomials:

sage: ore_polring = phi.ore_polring()
sage: t = ore_polring.gen()
sage: rho_T = z + t^3
sage: rho = DrinfeldModule(A, rho_T)
sage: rho
Drinfeld module defined by T |--> t^3 + z
sage: rho(T) == rho_T
True

Images under the Drinfeld module are computed by calling the object:

sage: phi(T)  # phi_T, the generator of the Drinfeld module
t^2 + t + z
sage: phi(T^3 + T + 1)  # phi_(T^3 + T + 1)
t^6 + (z^11 + z^9 + 2*z^6 + 2*z^4 + 2*z + 1)*t^4 + (2*z^11 + 2*z^10 + z^9 + z^8 + 2*z^7 + 2*z^6 + z^5 + 2*z^3)*t^3 + (2*z^11 + z^10 + z^9 + 2*z^7 + 2*z^6 + z^5 + z^4 + 2*z^3 + 2*z + 2)*t^2 + (2*z^11 + 2*z^8 + 2*z^6 + z^5 + z^4 + 2*z^2)*t + z^3 + z + 1
sage: phi(1)  # phi_1
1

The category of Drinfeld modules

Drinfeld modules have their own category (see class sage.categories.drinfeld_modules.DrinfeldModules):

sage: phi.category()
Category of Drinfeld modules over Finite Field in z of size 3^12 over its base
sage: phi.category() is psi.category()
False
sage: phi.category() is rho.category()
True

One can use the category to directly create new objects:

sage: cat = phi.category()
sage: cat.object([z, 0, 0, 1])
Drinfeld module defined by T |--> t^3 + z

The base field of a Drinfeld module

The base field of the Drinfeld module is retrieved using base():

sage: phi.base()
Finite Field in z of size 3^12 over its base

The base morphism is retrieved using base_morphism():

sage: phi.base_morphism()
Ring morphism:
  From: Univariate Polynomial Ring in T over Finite Field in z2 of size 3^2
  To:   Finite Field in z of size 3^12 over its base
  Defn: T |--> z

Note that the base field is not the field \(K\). Rather, it is a ring extension (see sage.rings.ring_extension.RingExtension) whose underlying ring is \(K\) and whose base is the base morphism:

sage: phi.base() is K
False

Getters

One can retrieve basic properties:

sage: phi.base_morphism()
Ring morphism:
  From: Univariate Polynomial Ring in T over Finite Field in z2 of size 3^2
  To:   Finite Field in z of size 3^12 over its base
  Defn: T |--> z
sage: phi.ore_polring()  # K{t}
Ore Polynomial Ring in t over Finite Field in z of size 3^12 over its base twisted by Frob^2
sage: phi.function_ring()  # Fq[T]
Univariate Polynomial Ring in T over Finite Field in z2 of size 3^2
sage: phi.gen()  # phi_T
t^2 + t + z
sage: phi.gen() == phi(T)
True
sage: phi.constant_coefficient()  # Constant coefficient of phi_T
z
sage: phi.morphism()  # The Drinfeld module as a morphism
Ring morphism:
  From: Univariate Polynomial Ring in T over Finite Field in z2 of size 3^2
  To:   Ore Polynomial Ring in t over Finite Field in z of size 3^12 over its base twisted by Frob^2
  Defn: T |--> t^2 + t + z

One can compute the rank and height:

sage: phi.rank()
2
sage: phi.height()
1

As well as the j-invariant if the rank is two:

sage: phi.j_invariant()  # j-invariant
1

A Drinfeld \(\mathbb{F}_q[T]\)-module can be seen as an Ore polynomial with positive degree and constant coefficient \(\gamma(T)\), where \(\gamma\) is the base morphism. This analogy is the motivation for the following methods:

sage: phi.coefficients()
[z, 1, 1]
sage: phi.coefficient(1)
1

Morphisms and isogenies

A morphism of Drinfeld modules \(\phi \to \psi\) is an Ore polynomial \(f \in K\{\tau\}\) such that \(f \phi_a = \psi_a f\) for every \(a\) in the function ring. In our case, this is equivalent to \(f \phi_T = \psi_T f\). An isogeny is a nonzero morphism.

Use the in syntax to test if an Ore polynomial defines a morphism:

sage: phi(T) in Hom(phi, phi)
True
sage: t^6 in Hom(phi, phi)
True
sage: t^5 + 2*t^3 + 1 in Hom(phi, phi)
False
sage: 1 in Hom(phi, rho)
False
sage: 1 in Hom(phi, phi)
True
sage: 0 in Hom(phi, rho)
True

To create a SageMath object representing the morphism, call the homset (hom):

sage: hom = Hom(phi, phi)
sage: frobenius_endomorphism = hom(t^6)
sage: identity_morphism = hom(1)
sage: zero_morphism = hom(0)
sage: frobenius_endomorphism
Endomorphism of Drinfeld module defined by T |--> t^2 + t + z
  Defn: t^6
sage: identity_morphism
Identity morphism of Drinfeld module defined by T |--> t^2 + t + z
sage: zero_morphism
Endomorphism of Drinfeld module defined by T |--> t^2 + t + z
  Defn: 0

The underlying Ore polynomial is retrieved with the method ore_polynomial():

sage: frobenius_endomorphism.ore_polynomial()
t^6
sage: identity_morphism.ore_polynomial()
1

One checks if a morphism is an isogeny, endomorphism or isomorphism:

sage: frobenius_endomorphism.is_isogeny()
True
sage: identity_morphism.is_isogeny()
True
sage: zero_morphism.is_isogeny()
False
sage: frobenius_endomorphism.is_isomorphism()
False
sage: identity_morphism.is_isomorphism()
True
sage: zero_morphism.is_isomorphism()
False

The Vélu formula

Let P be a nonzero Ore polynomial. We can decide if P defines an isogeny with a given domain and, if it does, find the codomain:

sage: P = (2*z^6 + z^3 + 2*z^2 + z + 2)*t + z^11 + 2*z^10 + 2*z^9 + 2*z^8 + z^7 + 2*z^6 + z^5 + z^3 + z^2 + z
sage: psi = phi.velu(P)
sage: psi
Drinfeld module defined by T |--> (2*z^11 + 2*z^9 + z^6 + 2*z^5 + 2*z^4 + 2*z^2 + 1)*t^2 + (2*z^11 + 2*z^10 + 2*z^9 + z^8 + 2*z^7 + 2*z^6 + z^5 + 2*z^4 + 2*z^2 + 2*z)*t + z
sage: P in Hom(phi, psi)
True
sage: P * phi(T) == psi(T) * P
True

If the input does not define an isogeny, an exception is raised:

sage: phi.velu(0)
Traceback (most recent call last):
...
ValueError: the input does not define an isogeny
sage: phi.velu(t)
Traceback (most recent call last):
...
ValueError: the input does not define an isogeny

The action of a Drinfeld module

The \(\mathbb{F}_q[T]\)-Drinfeld module \(\phi\) induces a special left \(\mathbb{F}_q[T]\)-module structure on any field extension \(L/K\). Let \(x \in L\) and \(a\) be in the function ring; the action is defined as \((a, x) \mapsto \phi_a(x)\). The method action() returns a sage.rings.function_field.drinfeld_modules.action.Action object representing the Drinfeld module action.

Note

In this implementation, \(L\) is \(K\):

sage: action = phi.action()
sage: action
Action on Finite Field in z of size 3^12 over its base induced by Drinfeld module defined by T |--> t^2 + t + z

The action on elements is computed by calling the action object:

sage: P = T + 1
sage: a = z
sage: action(P, a)
...
z^9 + 2*z^8 + 2*z^7 + 2*z^6 + 2*z^3 + z^2
sage: action(0, K.random_element())
0
sage: action(A.random_element(), 0)
0

Warning

The class DrinfeldModuleAction may be replaced later on. See issues #34833 and #34834.

action()#

Return the action object (sage.rings.function_field.drinfeld_modules.action.Action) that represents the module action, on the base codomain, that is induced by the Drinfeld module.

OUTPUT: a Drinfeld module action object

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: action = phi.action()
sage: action
Action on Finite Field in z12 of size 5^12 over its base induced by Drinfeld module defined by T |--> z12^5*t^2 + z12^3*t + 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

The action on elements is computed as follows:

sage: P = T^2 + T + 1
sage: a = z12 + 1
sage: action(P, a)
3*z12^11 + 2*z12^10 + 3*z12^9 + 3*z12^7 + 4*z12^5 + z12^4 + z12^3 + 2*z12 + 1
sage: action(0, a)
0
sage: action(P, 0)
0
coefficient(n)#

Return the \(n\)-th coefficient of the generator.

INPUT:

  • n – a nonnegative integer

OUTPUT: an element in the base codomain

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.coefficient(0)
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.coefficient(0) == p_root
True
sage: phi.coefficient(1)
z12^3
sage: phi.coefficient(2)
z12^5
sage: phi.coefficient(5)
Traceback (most recent call last):
...
ValueError: input must be >= 0 and <= rank
coefficients(sparse=True)#

Return the coefficients of the generator, as a list.

If the flag sparse is True (default), only return the nonzero coefficients; otherwise, return all of them.

INPUT:

  • sparse – a boolean

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.coefficients()
[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,
 z12^3,
 z12^5]

Careful, the method only returns the nonzero coefficients, unless otherwise specified:

sage: rho = DrinfeldModule(A, [p_root, 0, 0, 0, 1])
sage: rho.coefficients()
[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,
 1]
sage: rho.coefficients(sparse=False)
[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,
 0,
 0,
 0,
 1]
gen()#

Return the generator of the Drinfeld module.

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.gen() == phi(T)
True
height()#

Return the height of the Drinfeld module if the function field characteristic is a prime ideal; raise ValueError otherwise.

The height of a Drinfeld module is defined when the function field characteristic is a prime ideal. In our case, this ideal is even generated by a monic polynomial \(\mathfrak{p}\) in the function field. Write \(\phi_\mathfrak{p} = a_s \tau^s + \dots + \tau^{r*\deg(\mathfrak{p})}\). The height of the Drinfeld module is the well-defined positive integer \(h = \frac{s}{\deg(\mathfrak{p})}\).

Note

See [Gos1998], Definition 4.5.8 for the general definition.

A rank two Drinfeld module is supersingular if and only if its height equals its rank.

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.height() == 1
True
sage: phi.is_ordinary()
True
sage: B.<Y> = Fq[]
sage: L = Frac(B)
sage: phi = DrinfeldModule(A, [L(2), L(1)])
sage: phi.height()
Traceback (most recent call last):
...
NotImplementedError: height not implemented in this case
sage: Fq = GF(343)
sage: A.<T> = Fq[]
sage: K.<z6> = Fq.extension(2)
sage: phi = DrinfeldModule(A, [1, 0, z6])
sage: phi.height()
2
sage: phi.is_supersingular()
True
is_finite()#

Return True if this Drinfeld module is finite, False otherwise.

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.is_finite()
True
sage: B.<Y> = Fq[]
sage: L = Frac(B)
sage: psi = DrinfeldModule(A, [L(2), L(1)])
sage: psi.is_finite()
False
j_invariant()#

Return the j-invariant of the Drinfeld module if the rank is two; raise a NotImplementedError otherwise.

Assume the rank is two. Write the generator \(\phi_T = \omega + g\tau + \Delta\tau^2\). The j-invariant is defined by \(\frac{g^{q+1}}{\Delta}\), \(q\) being the order of the base field of the function ring. In our case, this field is always finite.

OUTPUT: an element in the base codomain

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.j_invariant()
z12^10 + 4*z12^9 + 3*z12^8 + 2*z12^7 + 3*z12^6 + z12^5 + z12^3 + 4*z12^2 + z12 + 2
sage: psi = DrinfeldModule(A, [p_root, 1, 1])
sage: psi.j_invariant()
1
sage: rho = DrinfeldModule(A, [p_root, 0, 1])
sage: rho.j_invariant()
0

The rank must be two:

sage: sigma = DrinfeldModule(A, [p_root, 1, 0])
sage: sigma.j_invariant()
Traceback (most recent call last):
...
NotImplementedError: rank must be 2
morphism()#

Return the morphism object that defines the Drinfeld module.

OUTPUT: a ring morphism from the function ring to the Ore polynomial 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.morphism()
Ring morphism:
  From: Univariate Polynomial Ring in T over Finite Field in z2 of size 5^2
  To:   Ore Polynomial Ring in t over Finite Field in z12 of size 5^12 over its base twisted by Frob^2
  Defn: T |--> z12^5*t^2 + z12^3*t + 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: from sage.rings.morphism import RingHomomorphism
sage: isinstance(phi.morphism(), RingHomomorphism)
True

Actually, the DrinfeldModule method __call__() simply class the __call__ method of this morphism:

sage: phi.morphism()(T) == phi(T)
True
sage: a = A.random_element()
sage: phi.morphism()(a) == phi(a)
True

And many methods of the Drinfeld module have a counterpart in the morphism object:

sage: m = phi.morphism()
sage: m.domain() is phi.function_ring()
True
sage: m.codomain() is phi.ore_polring()
True
sage: m.im_gens()
[z12^5*t^2 + z12^3*t + 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(T) == m.im_gens()[0]
True
rank()#

Return the rank of the Drinfeld module.

In our case, the rank is the degree of the generator.

OUTPUT: an integer

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.rank()
2
sage: psi = DrinfeldModule(A, [p_root, 2])
sage: psi.rank()
1
sage: rho = DrinfeldModule(A, [p_root, 0, 0, 0, 1])
sage: rho.rank()
4
velu(isog)#

Return a new Drinfeld module such that input is an isogeny to this module with domain self; if no such isogeny exists, raise an exception.

INPUT:

  • isog – the Ore polynomial that defines the isogeny

OUTPUT: a Drinfeld module

ALGORITHM:

The input defines an isogeny if only if:

1. The degree of the characteristic divides the height of the input. (The height of an Ore polynomial \(P(\tau)\) is the maximum \(n\) such that \(\tau^n\) right-divides \(P(\tau)\).)

2. The input right-divides the generator, which can be tested with Euclidean division.

We test if the input is an isogeny, and, if it is, we return the quotient of the Euclidean division.

Height and Euclidean division of Ore polynomials are implemented as methods of class sage.rings.polynomial.ore_polynomial_element.OrePolynomial.

Another possible algorithm is to recursively solve a system, see arXiv 2203.06970, Eq. 1.1.

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: t = phi.ore_polring().gen()
sage: isog = t + 2*z12^11 + 4*z12^9 + 2*z12^8 + 2*z12^6 + 3*z12^5 + z12^4 + 2*z12^3 + 4*z12^2 + 4*z12 + 4
sage: psi = phi.velu(isog)
sage: psi
Drinfeld module defined by T |--> (z12^11 + 3*z12^10 + z12^9 + z12^7 + z12^5 + 4*z12^4 + 4*z12^3 + z12^2 + 1)*t^2 + (2*z12^11 + 4*z12^10 + 2*z12^8 + z12^6 + 3*z12^5 + z12^4 + 2*z12^3 + z12^2 + z12 + 4)*t + 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: isog in Hom(phi, psi)
True

This method works for endomorphisms as well:

sage: phi.velu(phi(T)) is phi
True
sage: phi.velu(t^6) is phi
True

The following inputs do not define isogenies, and the method returns None:

sage: phi.velu(0)
Traceback (most recent call last):
...
ValueError: the input does not define an isogeny
sage: phi.velu(t)
Traceback (most recent call last):
...
ValueError: the input does not define an isogeny
sage: phi.velu(t^3 + t + 2)
Traceback (most recent call last):
...
ValueError: the input does not define an isogeny