class QuotientRing(CommutativeRing, metaclass=template("_ring", "_modulus")): """ A ring of residue classes of ring elements modulo a fixed ring element; the residue classes (quotient ring elements) support infix notation for ring operations, as well as mixed argument types. This is a template class that must be instantiated with the source ring and modulus. Use it, for example, as follows: @code # Instantiate the template; Z4 is a class (here: integer congruence classes) Z4 = QuotientRing( rings.integers.naive.Integers, 4 ) x = Z4(1) # Create a residue class: (1 mod 4) y = Z4(3) # Another residue class: (2 mod 4) z = x+y # z is (0 mod 4) z == Z4(2) * 2 # This true because 4 == 0 modulo 4 type(z) is Z4 # This is also true @endcode Other examples of QuotientRing template instances are: @code Z17 = QuotientRing( rings.integers.naive.Integers, 17 ) # Z/17Z # The ring (Z/2Z[x]) / (x^3 + x + 1) is isomorphic to GF8, # the field with 8 elements GF2 = fields.finite.naive.FiniteField( 2 ) GF2x = rings.polynomials.naive.Polynomials( GF2 ) GF8 = QuotientRing( GF2x, GF2x(1, 1, 0, 1) ) @endcode A quotient ring, factor ring, or residue class ring, is a ring @f$ R/(m) @f$, where @f$ R @f$ is the source ring, and @f$ (m) @f$ denotes the ideal generated by the element @f$ m\in R @f$. The ring operations carry over to quotient classes in a natural way: if @f$ x + y = z @f$ in @f$ R @f$, then @f$ [x] + [y] = [z] @f$ in @f$ R/(m) @f$ (by the canonical homomorphism). With the quotient ring operations being defined in terms of source ring operations, elements of the source ring must implement: - __bool__(): Zero testing (@c True if not zero) - __eq__(): Equality testing with the @c == operator (@c True if equal) - __add__(): Addition with the @c + operator; @c self is the left summand - __neg__(): Negation with the @c - unary minus operator (the additive inverse) - __mul__(): Multiplication with the @c * operator; @c self is the left factor - __divmod__(): Division with remainder; @c self is the dividend (left element) @note The implementation emphasizes simplicity over speed; it omits possible optimizations. @note The class uses the operand_casting() decorator: @c other operands in binary operations will first be treated as QuotientRing elements. If that fails, the operation will be repeated after @p other was fed to the constructor __init__(). If that fails, too, then the operation returns @c NotImplemented (so that @p other.__rop__() might be invoked). @see For example, Robinson, Derek J. S., "An Introduction to Abstract Algebra", p. 106. """ #- Instance Methods ----------------------------------------------------------- def __init__(self, representative): """ Construct a new residue class @p representative modulo modulus(). If the @p representative already is an element of this QuotientRing class, then the new element is a copy of @p representative. """ if isinstance(representative, self.__class__): self.__remainder = representative.__remainder elif isinstance(representative, self._modulus.__class__): self.__remainder = representative % self._modulus else: m = self._modulus self.__remainder = m.__class__(representative) % m def remainder(self): """ Return the remainder of the residue class (QuotientRing element) @p self. This is an element of the source ring(), not a residue class. """ return self.__remainder def __bool__(self): """ Test whether the residue class (QuotientRing element) is non-zero: return @c True if, and only if, the remainder modulo modulus() is non-zero. Return @c False if the remainder is zero. Implicit conversions to boolean (truth) values use this method, for example when @c x is an element of a QuotientRing: @code if x: do_something() @endcode """ return bool(self.__remainder) def __eq__(self, other): """ Test whether another residue class (QuotientRing element) @p other is equivalent to @p self; return @c True if that is the case. The infix operator @c == calls this method. Two residue classes @f$ [x], [y] @f$ are equal if, and only if, the difference of two representatives is a multiple of the modulus(): @f$ x-y = m\cdot z @f$. """ # The representative is always the remainder; an equality test suffices return self.__remainder == other.remainder() def __add__(self, other): """ Return the sum of @p self and @p other. The infix operator @c + calls this method. The sum of two residue classes (QuotientRing elements) @f$ [x], [y] @f$ is the residue class @f$ [x + y] @f$. """ return self.__class__(self.__remainder + other.remainder()) def __neg__(self): """ Return the additive inverse of @p self, which is @f$ [-x] @f$ for a residue class (QuotientRing element) @f$ [x] @f$. The negation operator @c -x (unary minus) calls this method. """ return self.__class__(-self.__remainder) def __mul__(self, other): """ Return the product of @p self and @p other. The infix operator @c * calls this method. The product of two residue classes (QuotientRing elements) @f$ [x], [y] @f$ is the residue class @f$ [x \cdot y] @f$. """ return self.__class__(self.__remainder * other.remainder()) def __truediv__(self, other): """ Return the quotient of @p self and @p other: multiply @p self with @c other.multiplicative_inverse(), so that @code (self / other) * other == self @endcode The infix operator @c / calls this method. @exception ZeroDivisionError if @p other is not a unit, that is, has no multiplicative inverse. """ return self * other.multiplicative_inverse() def __rtruediv__(self, other): """ Return the quotient of @p other and @p self: multiply @p other with @c self.multiplicative_inverse(), so that @code (other / self) * self == other @endcode The infix operator @c / calls this method if other.__truediv__() returned @c NotImplemented. @exception ZeroDivisionError if @p other is not a unit, that is, has no multiplicative inverse. """ return other * self.multiplicative_inverse() def multiplicative_inverse(self): """ Return an residue class (QuotientRing element) @c n such that @c n * self is one(). @exception ZeroDivisionError if @p self is not a unit, that is, has no multiplicative inverse. """ if not self.__remainder: raise ZeroDivisionError inverse, ignore, gcd = \ extended_euclidean_algorithm(self.__remainder, self._modulus) if gcd == self._ring.one(): return self.__class__(inverse) else: message = "element has no inverse: representative and modulus " \ "are not relatively prime" raise ZeroDivisionError(message) #- Class Methods----------------------------------------------------------- @classmethod def modulus(cls): """ Return the quotient ring's modulus; this is an element of the source ring(), not a residue class (QuotientRing element). """ return cls._modulus @classmethod def ring(cls): """ Return the source ring. """ return cls._ring @classmethod def zero(cls): """ Return the quotient ring's neutral element of addition: the residue class (QuotientRing element) of ring().zero() """ return cls(cls._ring.zero()) @classmethod def one(cls): """ Return the quotient ring's neutral element of multiplication: the residue class (QuotientRing element) of ring().one() """ return cls(cls._ring.one())
class LTorsionGroup(metaclass=template("_elliptic_curve")): """ An l-torsion group of an elliptic curve for odd l; use the @c elements() method to have intuitive syntax when working with l-torsion points. This is a template class that must be instantiated with the elliptic curve. Use it, for example, as follows: @code # Create the underlying elliptic curve E = elliptic_curves.naive.EllipticCurve( FiniteField(7), 3, 4 ) # Instantiate the template; El is a class (here: l-torsion groups of E) El = LTorsionGroup( E ) # Construct the group of 3-torsion points and do something with its points for P in El[3].elements(): do_something(P) @endcode The l-Torsion points of an elliptic curve are the points of order l. Their coordinates come from the algebraic closure of the elliptic curve's field, which makes them badly suited for computer representation. (Extending the coordinate domain to the algebraic closure makes sure that all points of order l are available; there are always @f$ l^2 @f$ points of order l if l is prime to the field characteristic.) @note The l-torsion points are represented implicitly through polynomial arithmetic. Therefore, the list of points in returned by @c elements() contains only a single entry: the point @f$ (x, y) \in E\bigl(\mathbb{F}_{p}(E) / \psi_l\bigr) @f$, where @f$ \mathbb{F}_{p}(E)/\psi_l @f$ denotes the field of rational functions over the elliptic curve @f$ E @f$ with numerator and denominator polynomials taken modulo the l-th division polynomial @f$ \psi_l @f$. @note The class supports only odd torsions greater one because multivariate polynomial arithmetic is unavailable @see Silverman, Joseph H., "The Arithmetic of Elliptic Curves", second edition, Springer, 2009, p. 373 """ #- Instance Methods ------------------------------------------------------- def __init__(self, torsion): """ Construct a new l-torsion group for the given @p torsion. @param torsion The @p torsion is the order of the points in the group; the group represents all points of order @p torsion. It must be an odd integer greater than 1 and prime to the field characteristic; the limitation comes from the implicit representation, see above. """ torsion = int(torsion) if torsion < 3 or torsion % 2 == 0 or self.curve().field()( torsion) == 0: raise ValueError("only odd torsions greater than 1 are supported") self.__torsion = torsion self.__point = None def elements(self): """ Return a list containing the one point that implicitly represents the whole group. Use it, for example, to elegantly perform an operation on all l-torsion points: @code for P in torsion_group.elements(): do_something(P) @endcode The one point in the list is @f$ (x, y) \in E\bigl(\mathbb{F}_{p}(E) / \psi_l\bigr) @f$, where @f$ \mathbb{F}_{p}(E)/\psi_l @f$ denotes the field of rational functions over the elliptic curve @f$ E @f$ with numerator and denominator polynomials taken modulo the l-th division polynomial @f$ \psi_l @f$. """ if not self.__point: self.__init_point() return [self.__point] def torsion(self): """ Return the group's torsion, that is, l for the l-torsion group. """ return self.__torsion def __init_point(self): """ Create the point that implicitly represents the whole l-torsion group and store it in the cache. Calling the method has no effect if the point already exists. """ if self.__point: return # R = F[x] / (y**2 - x**3 - A*x - B) R = self._division_polynomial_list().curve_polynomials() # The n-th division polynomial psi = self._division_polynomial_list()[self.__torsion] # T = ( F[x] / (y**2 - x**3 - A*x - B) ) / psi(l) S = QuotientRing(R, psi) T = FractionField(S) A, B = R.curve().parameters() # Polynomials x and y on the curve interpreted # as elements of the field of fractions x = T(R((0, 1), 0)) y = T(R(0, 1)) self.__point = EllipticCurve(T, A, B)(x, y) #- Class Methods----------------------------------------------------------- @classmethod def curve(cls): """ Return the elliptic curve that was used to initialize the template. """ return cls._elliptic_curve @classmethod def _division_polynomial_list(cls): """ Return the list of division polynomials that supplies the modulus for the implicit polynomial representation of the l-torsion points. """ try: return cls.__division_polynomial_list except AttributeError: # R = F[x] / (y**2 - x**3 - A*x - B) # where F, A, B are the elliptic curve's field and its parameters. R = CurvePolynomials(cls._elliptic_curve) cls.__division_polynomial_list = DivisionPolynomialsList(R) return cls.__division_polynomial_list
class Polynomials(CommutativeRing, metaclass=template("_coefficient_field")): """ A ring of polynomials in one indeterminate with coefficients from the provided field; the polynomials support infix notation for ring operations, as well as mixed argument types. This is a template class that must be instantiated with the coefficient field. Use it, for example, as follows: @code # Instantiate the template; GF7x is a class (here: polynomials over GF7) GF7 = fields.finite.naive.FiniteField( 7 ) GF7x = Polynomials( GF7 ) p = GF7x(1, 1) # Create a polynomial: x + 1 q = GF7x(-1, 1) # Another polynomial: x - 1 p * q == GF7x(-1, 0, 1) # This is true: (x + 1)(x - 1) = x^2 - 1 type(p) is GF7x # This is also true # Use the polynomial 'x' for a more natural syntax x = GF7(0, 1) x**3 - 2*x + 1 == GF7x(1, -2, 0, 1) @endcode The polynomial ring operations are defined in terms of the coefficient field operations. For everything to work as expected, the template argument must be a class whose instances implement the fields.Field interface. Typically, this will be some specialization of fields.finite.naive.FiniteField. Rings, however, also work as a source of coefficients. Only division requires that the leading coefficient is a unit. @note The implementation emphasizes simplicity over speed; it omits possible optimizations. @note The class uses the operand_casting() decorator: @c other operands in binary operations will first be treated as Polynomial elements. If that fails, the operation will be repeated after @p other was fed to the constructor __init__(). If that fails, too, then the operation returns @c NotImplemented (so that @p other.__rop__() might be invoked). @see For example, Robinson, Derek J. S., "An Introduction to Abstract Algebra", p. 100. """ #- Instance Methods ----------------------------------------------------------- def __init__(self, element_description, *further_coefficients): """ Create a new polynomial from the given @p element_description. @param element_description Valid input values are: - A Polynomial over the same field: then the polynomial is copied and @p further_coefficients is ignored. - An iterable object (with an @c __iter__() method): then the polynomial coefficients are read from the iterable and, again, @p further_coefficients is ignored. The list must be in ascending order: first the constant, then the linear, quadratic, and cubic coefficients; and so on. - Any number of arguments that can be interpreted as coefficient field elements; they will be combined into the list of coefficients @param further_coefficients The list of variable positional arguments. For example, there are several ways to construct the polynomial @f$ x^3 + 4x^2 - 3x + 2@f$: @code # Suppose R is a Polynomials template specialization # List of coefficients p2 = R( [2, -3, 4, 1] ) # Variable argument count p3 = R( 2, -3, 4, 1 ) # Copy p1 = R( p2 ) @endcode @note Leading zeros will be ignored. """ # List of coefficients is in ascending order without leading zeros. # Example: x^2 + 2x + 5 = [5, 2, 1] if isinstance(element_description, self.__class__): # A reference suffices because objects are immutable self.__coefficients = element_description.__coefficients else: if type(element_description) in [list, tuple]: coefficients = element_description else: coefficients = [element_description ] + list(further_coefficients) F = self._coefficient_field self.__coefficients = [F(c) for c in coefficients] self.__remove_leading_zeros() def coefficients(self): """ Return a copy of the list of coefficients. The list is in ascending order: it starts with the constant coefficient, then the linear, quadratic, and cubic coefficients; and so on. It ends with the leading coefficient. """ return self.__coefficients[:] def leading_coefficient(self): """ Return the leading coefficient, or zero if @p self is the zero polynomial. """ if self.__coefficients: return self.__coefficients[-1] else: return self._coefficient_field.zero() def degree(self): """ Return the degree of the polynomial. @note The zero polynomial has degree @f$ -\infty @f$; however, this implementation returns the number @f$ -2^{30} @f$ to avoid special constructions that side-step Python's @c int objects. For practical purposes, this should suffice. """ if not self.__coefficients: # FIXME: The degree of the zero polynomial is minus infinity. # This will, however, do for now. return -(2**30) return len(self.__coefficients) - 1 def __bool__(self): """ Test whether the polynomial is non-zero: return @c True if, and only if, at least one coefficient is non-zero. Return @c False if all coefficients are zero. Implicit conversions to boolean (truth) values use this method, for example when @c x is an element of Polynomials: @code if x: do_something() @endcode """ return bool(self.__coefficients) def __eq__(self, other): """ Test whether another polynomial @p other is equal to @p self; return @c True if that is the case. The infix operator @c == calls this method. Two polynomials are equal if, and only if, all their coefficients are equal. """ if self.degree() != other.degree(): return False # zip() is OK because we have identical length for x, y in zip(self.__coefficients, other.__coefficients): if x != y: return False return True def __add__(self, other): """ Return the sum of @p self and @p other. The infix operator @c + calls this method. Polynomials add coefficient-wise without carrying: the sum of two polynomials @f$ \sum_{k} a_{k}x^{k} @f$ and @f$ \sum_{k} b_{k}x^{k} @f$ is the polynomial @f$ \sum_{k} (a_{k} + b_{k})x^{k} @f$. """ zero = self._coefficient_field.zero() coefficient_pairs = self.__pad_and_zip(self.__coefficients, other.__coefficients, zero) coefficient_sums = [x + y for x, y in coefficient_pairs] return self.__class__(coefficient_sums) def __neg__(self): """ Return the additive inverse (the negative) of @p self. A polynomial is negated by negating its coefficients: the additive inverse of @f$ \sum_{k} a_{k}x^{k} @f$ is @f$ \sum_{k} -a_{k}x^{k} @f$. """ return self.__class__([-c for c in self.__coefficients]) def __mul__(self, other): """ Return the product of @p self and @p other. The infix operator @c * calls this method. Two polynomials are multiplied through convolution of their coefficients: for polynomials @f$ \sum_{k} a_{k}x^{k} @f$ and @f$ \sum_{k} b_{k}x^{k} @f$, their product is the polynomial @f$ \sum_{k} \sum_{j=0}^{k}(a_{j} + b_{k-j})x^{k} @f$. """ # Initialize result as list of all zeros zero = self._coefficient_field.zero() # Add 2 because degrees count from 0. result = [zero] * (self.degree() + other.degree() + 2) for i, x in enumerate(self.__coefficients): for j, y in enumerate(other.__coefficients): result[i + j] += x * y return self.__class__(result) def __divmod__(self, other): """ Return the quotient and remainder of @p self divided by @p other. The built-in method @c divmod() calls this method. For the returned values @c quotient and @c remainder, we have @code self == quotient * other + remainder @endcode """ # Lists will be modified, so copy them dividend = self.__coefficients[:] divisor = other.__coefficients[:] n = other.degree() zero = self._coefficient_field.zero() quotient = [zero] * (self.degree() - n + 1) for k in reversed(range(0, len(quotient))): quotient[k] = dividend[n + k] / divisor[n] for j in range(k, n + k): dividend[j] -= quotient[k] * divisor[j - k] remainder = dividend[0:n] return self.__class__( quotient ), \ self.__class__( remainder ) def __call__(self, point): """ Return the polynomial function's value at @p point. This method evaluates the polynomial by replacing the indeterminate with @p point and summing the powers: for a polynomial @f$ p(x) = \sum_{k} a_{k}x^{k} @f$, the value at @f$ c @f$ is @f$ p(c) = \sum_{k} a_{k}c^{k} @f$. """ return sum([c * point**i for i, c in enumerate(self.__coefficients)]) #- Class Methods----------------------------------------------------------- @classmethod def coefficient_field(cls): """ Return the coefficient field. """ return cls._coefficient_field @classmethod def zero(cls): """ Return the polynomial ring's neutral element of addition: the zero polynomial. """ return cls(cls._coefficient_field.zero()) @classmethod def one(cls): """ Return the polynomial ring's neutral element of multiplication: the constant polynomial one. """ return cls(cls._coefficient_field.one()) #- Auxiliary Functions ---------------------------------------------------- def __remove_leading_zeros(self): """ Remove all leading zeros from the list of coefficients. This might empty the list completely. """ while len( self.__coefficients ) > 0 \ and not self.__coefficients[-1]: self.__coefficients.pop() @staticmethod def __pad_and_zip(list1, list2, padding_element): """ Combine @p list1 and @p list1 into a single list of pairs. If the lists have different lengths, pad the shorter list with @p padding_element: fill in elements until they have the same length. """ max_length = max(len(list1), len(list2)) padded_list1 = list1 + ([padding_element] * (max_length - len(list1))) padded_list2 = list2 + ([padding_element] * (max_length - len(list2))) return zip(padded_list1, padded_list2)
class FractionField(Field, metaclass=template("_integral_domain")): """ A field of formal quotients (fractions) @f$ \frac{\mathrm{numerator}}{\mathrm{denominator}} @f$ over an integral domain; the field elements support infix notation for field operations, and mixed argument types. This is a template class that must be instantiated with the underlying integral domain. For example: @code # Instantiate the template; Q is a class (here: the rational numbers) Q = FractionField( rings.integers.naive.Integers ) x = Q(1, 2) # Create a field element: one over two y = Q(2) # Another field element: two, which is two over one z = x*y # z is two over two; no canceling takes place z == Q(4, 4) # This true because 4*2 == 2*4 type(z) is Q # This is also true @endcode A formal quotient is an equivalence class of pairs (numerator, denominator) whose entries come from an integral domain (a commutative ring with identity and no zero divisors). Two pairs @f$ \frac{u}{v} @f$ and @f$ \frac{s}{t} @f$ are considered equivalent if, and only if, @f$ u\cdot t = s\cdot v @f$. Observe that this comparison uses multiplication only; since the elements come from a ring and might have no multiplicative inverses, division does not work. @note A consequence of omitted canceling is that the elements might grow large. @note The class uses the operand_casting() decorator: @c other operands in binary operations will first be treated as FractionField elements. If that fails, the operation will be repeated after @p other was fed to the constructor __init__(). If that fails, too, then the operation returns @c NotImplemented (so that @p other.__rop__() might be invoked). @see For example, Robinson, Derek J. S., "An Introduction to Abstract Algebra", p. 113. """ def __init__(self, numerator, denominator=None): """ Construct a new formal quotient (@p numerator, @p denominator). If the @p numerator is an element of this FractionField class, then the new element is a copy of @p numerator; @p denominator is ignored. Otherwise, a @c None denominator (the default) is interpreted as the multiplicative neutral element (one) in the underlying integral domain. @exception ZeroDivisonError if the denominator is zero (and not @c None, see above). """ if isinstance(numerator, self.__class__): # Copy an instance self.__numerator = numerator.__numerator self.__denominator = numerator.__denominator else: if denominator is None: denominator = self._integral_domain.one() if not denominator: raise ZeroDivisionError self.__numerator = self._integral_domain(numerator) self.__denominator = self._integral_domain(denominator) def __bool__(self): """ Test whether the formal quotient is non-zero: return @c True if, and only if, the numerator is non-zero. Return @c False if the numerator is zero. Implicit conversions to boolean (truth) values use this method, for example when @c x is an element of a FractionField: @code if x: do_something() @endcode """ return bool(self.__numerator) def __eq__(self, other): """ Test whether another formal quotient @p other is equivalent to @p self; return @c True if that is the case. The infix operator @c == calls this method. Two fractions @f$ \frac{u}{v} @f$ and @f$ \frac{s}{t} @f$ are equivalent if, and only if, @f$ u\cdot t = s\cdot v @f$. @note Comparison may be expensive: it requires two multiplications in the underlying integral domain. """ # Use the basic definition of equivalence for comparison. return self.__numerator * other.__denominator \ == other.__numerator * self.__denominator def __add__(self, other): """ Return the sum of @p self and @p other. The infix operator @c + calls this method. As usual, the sum of two fractions @f$ \frac{u}{v} @f$ and @f$ \frac{s}{t} @f$ is @f$ \frac{ u\cdot t + s\cdot v }{ v\cdot t } @f$. @note Canceling of common factors is impossible, for the underlying integral domain contains non-units. Therefore, repeated addition results in large elements. """ numerator = self.__numerator * other.__denominator \ + self.__denominator * other.__numerator denominator = self.__denominator * other.__denominator return self.__class__(numerator, denominator) def __neg__(self): """ Return the additive inverse of @p self, which is @f$ \frac{-u}{v} @f$ for a fraction @f$ \frac{u}{v} @f$. The negation operator @c -x (unary minus) calls this method. """ return self.__class__(-self.__numerator, self.__denominator) def __mul__(self, other): """ Return the product of @p self and @p other. The @c * infix operator calls this method. As usual, the product of two fractions @f$ \frac{u}{v} @f$ and @f$ \frac{s}{t} @f$ is @f$ \frac{ u\cdot s }{ v\cdot t } @f$. @note Canceling of common factors is impossible, for the underlying integral domain contains non-units. Therefore, repeated multiplication results in large elements. """ numerator = self.__numerator * other.__numerator denominator = self.__denominator * other.__denominator return self.__class__(numerator, denominator) def multiplicative_inverse(self): """ Return the multiplicative inverse of @p self, which is @f$ \frac{v}{u} @f$ for a fraction @f$ \frac{u}{v} @f$. @exception ZeroDivisionError if @p self is zero (has a zero numerator) @see __bool__() """ if not self: raise ZeroDivisionError return self.__class__(self.__denominator, self.__numerator) @classmethod def zero(cls): """ Return the field's neutral element of addition (zero). Zero in a FractionField is a fraction @f$ \frac{0}{1} @f$, where @f$ 0 @f$ and @f$ 1 @f$ denote the zero and one of the underlying integral domain. """ return cls(cls._integral_domain.zero(), cls._integral_domain.one()) @classmethod def one(cls): """ Return the field's neutral element of multiplication (one). One (or unity) in a FractionField is a fraction @f$ \frac{1}{1} @f$, where @f$ 1 @f$ denotes the one of the underlying integral domain. """ return cls(cls._integral_domain.one(), cls._integral_domain.one())
class CurvePolynomials(CommutativeRing, metaclass=template("_elliptic_curve")): """ A ring of polynomials over an elliptic curve; the polynomials support infix notation for ring operations, as well as mixed argument types. This is a template class that must be instantiated with the elliptic curve. Use it, for example, as follows: @code # Create the underlying elliptic curve E = elliptic_curves.naive.EllipticCurve( FiniteField(7), 3, 4 ) # Instantiate the template; GF7E is a class (here: polynomials over E) GF7E = CurvePolynomials( E ) # Use two coefficient lists as arguments; the polynomial is (list1) + y*(list2) s = GF7E( (4, 3, 0, 1) ) # The curve's defining polynomial y = GF7E( 0, (0, 1) ) # Another polynomial: y y**2 == s # The fundamental relation holds type(s) is GF7E # This is also true # Use the polynomials 'x' and 'y' for a more natural syntax x = GF7( (0, 1), () ) # The empty tuple is just for emphasis y = GF7( (), (0, 1) ) # The empty tuple is just for emphasis y**2 == x**3 + 3**x + 4 p = x**2 + 2* x + y*( 3 * x**5 + 6 * x**2 ) # A (pseudo) random polynomial @endcode The ring of polynomials over an elliptic curve @f$ E @f$ is @f$ \mathbb{F}[x,y]/(y^2 - x^3 - Ax - B) @f$, where @f$ \mathbb{F} @f$ is the field over which the curve was defined, and @f$ A, B @f$ are the curve parameters. The quotient polynomial @f$ y^2 - x^3 - Ax - B @f$ expresses that the polynomial's domain is the set of points on @f$ E @f$; the (Cartesian) coordinates of the points @f$ (x, y) \in E @f$ are connected by the fundamental relation @f$ y^2 = x^3 + Ax + B @f$. @note As a consequence of the fundamental relation, powers of @f$ y @f$ greater than one can be reduced to a polynomial in @f$ x @f$. This is what this implementation does. Since it does not provide full multivariate arithmetic, division (with remainder; see __divmod__()) only works in the following cases: - The divisor is a polynomial in @f$ x @f$ alone; this works for all dividends. - Dividend and divisor are polynomials in @f$ y @f$ alone. Besides restricting arithmetic, the implementation emphasizes simplicity over speed; it omits possible optimizations. @note The class uses the operand_casting() decorator: @c other operands in binary operations will first be treated as CurvePolynomial elements. If that fails, the operation will be repeated after @p other was fed to the constructor __init__(). If that fails, too, then the operation returns @c NotImplemented (so that @p other.__rop__() might be invoked). @see Charlap, Leonard S. and Robbins, David P., "CRD Expositroy Report 31: An Elementary Introduction to Elliptic Curves", 1988, chapter 3 """ #- Instance Methods ----------------------------------------------------------- def __init__(self, x_factor, y_factor=None): """ Create a new polynomial from the given coefficient lists @p x_factor and @p y_factor. Polynomials @f$ s @f$ over elliptic curves have a canonical form @f$ s(x, y) = a(x) + y\cdot b(x) @f$, where @f$ a, b @f$ are polynomials of @f$ x @f$ alone. The parameter @p x_factor determines the coefficients of @f$ a @f$, @p y_factor determines the coefficients of @f$ b @f$. @param x_factor An iterable that lists the coefficients of the polynomial @f$ a @f$ in ascending order: @p x_factor[0] is the constant, @p x_factor[1] the linear, @p x_factor[2] the quadratic coeffiecient; and so on. @param y_factor Similar to @p x_factor, but for the polynomial @f$ b @f$. For example, construct the polynomial @f$ x^3 + 4x - y\cdot(3x + 2) @f$ as follows: @code # Suppose E is an is an EllipticCurve template specialization R = CurvePolynomials( E ) p = R( (0, 4, 0, 1), (2, 3) ) @endcode @note Leading zeros in the coefficient lists will be ignored. """ # x_factor: polynomial in x only # y_factor: polynomial in x [implicitly * y] if isinstance(x_factor, self.__class__): # Casting an object that already has this type means copying it. self.__x_factor = x_factor.__x_factor self.__y_factor = x_factor.__y_factor else: self.__x_factor = self.polynomial_ring()(x_factor) if y_factor is None: self.__y_factor = self.polynomial_ring().zero() else: self.__y_factor = self.polynomial_ring()(y_factor) def x_factor(self): """ Return the polynomial @f$ a @f$ from the canonical form @f$ s(x, y) = a(x) + y\cdot b(x) @f$ of @p self. @note The returned polynomial is univariate. """ return self.__x_factor def y_factor(self): """ Return the polynomial @f$ b @f$ from the canonical form @f$ s(x, y) = a(x) + y\cdot b(x) @f$ of @p self. @note The returned polynomial is univariate. """ return self.__y_factor def leading_coefficient(self): """ Return the leading coefficient, or zero if @p self is the zero polynomial. @note The fundamental relation @f$ y^2 = x^3 + Ax + B @f$ forces the linear polynomials @f$ x @f$ and @f$ y @f$ to have different degrees. Thus @f$ \deg(x) = 2 @f$ and @f$ \deg(y) = 3 @f$. The leading coefficient is the coefficient of the power with the highest degree; for @f$ x^4 + 2y^3 @f$ it is 2. (Ignore the reduction of @f$ y^2 @f$ for a moment.) """ # Equality is impossible: we compare an odd and an even number. if (2 * self.__x_factor.degree()) > (3 + 2 * self.__y_factor.degree()): return self.__x_factor.leading_coefficient() else: return self.__y_factor.leading_coefficient() def __bool__(self): """ Test whether the polynomial is non-zero: return @c True if, and only if, at least one coefficient is non-zero. Return @c False if all coefficients are zero. Implicit conversions to boolean (truth) values use this method, for example when @c x is an element of Polynomials: @code if x: do_something() @endcode """ return bool(self.__x_factor) or bool(self.__y_factor) def __eq__(self, other): """ Test whether another polynomial @p other is equal to @p self; return @c True if that is the case. The infix operator @c == calls this method. Two polynomials are equal if, and only if, all the coefficients of their canonical forms are equal. """ return self.__x_factor == other.x_factor() \ and self.__y_factor == other.y_factor() def __add__(self, other): """ Return the sum of @p self and @p other. The infix operator @c + calls this method. Polynomials add coefficient-wise without carrying: the sum of two polynomials @f$ \sum_{k,l} a_{k,l}x^{k}y^{l} @f$ and @f$ \sum_{k,l} b_{k,l}x^{k}y^{l} @f$ is the polynomial @f$ \sum_{k,l} (a_{k,l} + b_{k,l})x^{k}y^{l} @f$. """ x = self.__x_factor + other.x_factor() y = self.__y_factor + other.y_factor() return self.__class__(x, y) def __neg__(self): """ Return the additive inverse (the negative) of @p self. A polynomial is negated by negating its coefficients: the additive inverse of @f$ \sum_{k,l} a_{k,l}x^{k}y^{l} @f$ is @f$ \sum_{k,l} -a_{k,l}x^{k}y^{l} @f$. """ return self.__class__( -self.__x_factor, -self.__y_factor ) def __mul__(self, other): """ Return the product of @p self and @p other. The infix operator @c * calls this method. Two polynomials are multiplied through convolution of their coefficients; however that is overly complicated to write with sums. Instead, simply imagine mutliplying the canonical forms @f$ a(x) + y \cdot b(x) @f$ and @f$ c(x) + y \cdot d(x) @f$. @see rings.polynomials.naive.Polynomials.__mul__() for an explanation of univariate polynomial multiplication. """ x = self.__x_factor * other.x_factor() xy = self.__x_factor * other.y_factor() yx = self.__y_factor * other.x_factor() y = xy + yx # Reduce y**2 to y2_reduction if self.__y_factor and other.y_factor(): # y = self.__y_factor - self.__y_factor # Zero of unknown type x += self.__y_factor * other.y_factor() * self.y2_reduction() return self.__class__(x, y) def __divmod__(self, other): """ Return the quotient and remainder of @p self divided by @p other. The built-in method @c divmod() calls this method. For the returned values @c quotient and @c remainder, we have @code self == quotient * other + remainder @endcode @note Division has limited functionality in this implementation. It only works in the following cases: - The divisor (@p other) is a polynomial in @f$ x @f$ alone; this works for all dividends. - Dividend (@p self) and divisor (@p other) are polynomials in @f$ y @f$ alone. """ if not other: raise ZeroDivisionError if not self: return self.polynomial_ring().zero(), self.polynomial_ring().zero() if other.y_factor() and (self.__x_factor or other.x_factor()): # Ok, this is cheating. Multivariate polynomial division is, # however, quite complicated and unnecessary for our purpose. raise ValueError("multivariate division is unsupported") if other.x_factor(): qx, rx = divmod(self.__x_factor, other.x_factor()) qy, ry = divmod(self.__y_factor, other.x_factor()) else: # Case: self and other have only y factors qx, rx = divmod(self.__y_factor, other.y_factor()) zero = self.polynomial_ring().zero().x_factor() qy, ry = zero, zero quotient = self.__class__(qx, qy) remainder = self.__class__(rx, ry) return quotient, remainder #- Class Methods----------------------------------------------------------- @classmethod def curve(cls): """ Return the elliptic curve over which the polynomial ring was defined. """ return cls._elliptic_curve @classmethod def y2_reduction(cls): """ Return the polynomial that may be substituted for @f$ y^2 @f$. For an elliptic curve with parameters @f$ A, B @f$, this is the polynomial @f$ x^3 + Ax + B @f$. """ try: return cls.__y2_reduction except AttributeError: # Use y**2 = x**3 + A*x + B to eliminate any y-power greater than 1. A, B = cls._elliptic_curve.parameters() cls.__y2_reduction = cls.polynomial_ring()(B, A, 0, 1) return cls.__y2_reduction @classmethod def polynomial_ring(cls): """ Return the ring of (univariate) polynomials that is source of the polynomials @f$ a @f$ and @f$ b @f$ in the canonical form @f$ s(x, y) = a(x) + y \cdot b(x) @f$. """ try: return cls.__polynomial_ring except AttributeError: cls.__polynomial_ring = Polynomials(cls._elliptic_curve.field()) return cls.__polynomial_ring @classmethod def zero(cls): """ Return the polynomial ring's neutral element of addition: the zero polynomial. """ R = cls.polynomial_ring() return cls(R.zero(), R.zero()) @classmethod def one(cls): """ Return the polynomial ring's neutral element of multiplication: the constant polynomial one. """ R = cls.polynomial_ring() return cls(R.one(), R.zero())
class EllipticCurve(metaclass=template("_field", "_A", "_B")): """ An elliptic curve; the points support additive infix notation for group operations, as well as multiplication by integers. This is a template class that must be instantiated with the field and the parameters @c A and @c B. Use it, for example, as follows: @code # Instantiate the elliptic curve template as y^2 = x^3 + 3x + 4 over GF(7) E = EllipticCurve( FiniteField(7), 3, 4 ) # Create two points and add them P = E(1, 1) Q = E(2, 5) P + Q == E(6, 0) # Point addition 3*P == E(0, 5) # Multiplication by integers O = PointAtInfinity() P == P + O # The point at infinity is the neutral element type(P) is E # This is also true @endcode An elliptic curve is the set of all points @f$ E = \bigl\{ (x, y) \mid y^2 = x^3 + Ax + B \bigr\} \cup \{\mathcal{O}\} @f$, where @f$ \mathcal{O} @f$ is a special element, the point at infinity. The points form an abelian group with the point at infinity as neutral element. The relation @f$ y^2 = x^3 + Ax + B @f$ between the (Cartesian) coordinates of the points @f$ (x, y) \in E @f$ is called the fundamental relation. @see Charlap, Leonard S. and Robbins, David P., "CRD Expositroy Report 31: An Elementary Introduction to Elliptic Curves", 1988, chapter 1 """ #- Instance Methods ----------------------------------------------------------- def __init__(self, x, y): """ Construct a new point on the elliptic curve with coordinates @p x and @p y. The coordinates must be connected by the fundamental relation @f$ y^2 = x^3 + Ax + B @f$. @param x The first coordinate of the point. It will be interpreted as an element of field(), which might result in a ValueError or TypeError. @param y The second coordinate of the point. It will be interpreted as an element of field(), which might result in a ValueError or TypeError. @exception AssertionError if the fundamental relation does not hold. @exception ValueError if an argument cannot be cast as element of field(). @exception TypeError same as @c ValueError. """ self.__x = self._field(x) self.__y = self._field(y) A, B = self.parameters() x, y = self.__x, self.__y assert y ** 2 == x ** 3 + A * x + B, \ "point ({x}, {y}) is not on the curve".format(x=x, y=y) def x(self): """ Return the first (Cartesian) coordinate of the point @f$ (x,y) @f$ on the curve. @note The fundamental relation links the coordinates: @f$ y^2 = x^3 + Ax + B @f$. """ return self.__x def y(self): """ Return the second (Cartesian) coordinate of the point @f$ (x,y) @f$ on the curve. @note The fundamental relation links the coordinates: @f$ y^2 = x^3 + Ax + B @f$. """ return self.__y def is_infinite(self): """ Test whether the point is infinite or not: always return @c False for the point is finite. The point at infinity returns @c True. """ return False def __bool__(self): """ Test whether the point is infinite or not: always return @c True, for the point is finite. The point at infinity returns @c False. Implicit conversions to boolean (truth) values use this method, for example when @c P is an element of EllipticCurve: @code if P: do_something() @endcode """ return True def __eq__(self, other): """ Test whether another point @p other is equal to @p self; return @c True if that is the case. The infix operator @c == calls this method. Two points are equal if, and only if, both of their (Cartesian) coordinates are equal or if they both are infinite. """ if other.is_infinite(): return False else: return self.__x == other.x() and self.__y == other.y() def __neq__(self, other): """ Test whether another point @p other is different from @p self; return @c True if that is the case. The infix operator @c != calls this method. Two points are different if, and only if, they differ in at least one of their (Cartesian) coordinates. """ return not self == other def __add__(self, other): """ Return the sum of @p self and @p other. The infix operator @c + calls this method. The geometric interpretation of point addition is to draw a line between the points and mirror the point at the third intersection of line and curve on the y-axis. However, the justification why this works is rather complicated. Please see some introductory text for the formulas and the respective proofs. @note Addition on elliptic curves is commutative. The neutral element is the point at infinity (compare PointAtInfinity). @see For example Washington, Lawrence C. "Elliptic Curves: Number Theory and Cryptography", second edition, CRC Press 2008, chapter 2. """ if other.is_infinite(): return self elif self == -other: return PointAtInfinity() else: if self.__x == other.x(): return self.__double__() else: return self.__generic_add__(other) def __generic_add__(self, other): """ Generic addition of points @p self and @p other: the points are neither identical nor inverses. @note This method should not be called directly. Instead use the infix operator @c +, which calls __add__(). @note Putting the cases in separate methods makes call profiles more expressive. @see __add__() """ gamma = (other.y() - self.__y) / (other.x() - self.__x) u = -self.__x - other.x() + gamma**2 v = -self.__y - gamma * (u - self.__x) return self.__class__(u, v) def __double__(self): """ Point doubling: add a point to itself. @note This method should not be called directly. Instead use the infix operator @c +, which calls __add__(). @note Putting the cases in separate methods makes call profiles more expressive. @see __add__() """ A, B = self.parameters() delta = (3 * self.__x**2 + A) / (2 * self.__y) u = -self.__x - self.__x + delta**2 v = -self.__y - delta * (u - self.__x) return self.__class__(u, v) def __neg__(self): """ Return the additive inverse (the negative) of @p self. The additive inverse of a point @f$ (x,y) @f$ on the curve is @f$ (x, -y) @f$. """ return self.__class__(self.__x, -self.__y) def __sub__(self, other): """ Return the difference between @p self and @p other. The infix operator @c - calls this method. This is the same as @p self + (-other). @see __add__() """ return self + (-other) def __mul__(self, n): """ Multiplication with integers: adding @p n copies of the point @p self; the infix operator @c * calls this method. This method is used when self was the left factor. @param n A number object that can be interpreted as an integer. It determines how often the point will be added to itself. @exception ValueError if @p n cannot be cast to @c int(). @exception TypeError same as @c ValueError. """ n = int(n) if n == 0: return PointAtInfinity() point = self for i in range(1, n): point += self return point def __rmul__(self, other): """ Multiplication with integers: adding @p n copies of the point @p self; the infix operator @c * calls this method. This method is used when self was the right factor. @param n A number object that can be interpreted as an integer. It determines how often the point will be added to itself. @exception ValueError if @p n cannot be cast to @c int(). @exception TypeError same as @c ValueError. """ # Multiplication with integers is always commutative. return self * other #- Class Methods----------------------------------------------------------- @classmethod def field(cls): """ Return the field over which the curve was defined. """ return cls._field @classmethod def parameters(cls): """ Return the parameter pair @f$ (A, B) @f$ of the curve. The parameters determine the fundamental relation @f$ y^2 = x^3 + Ax + B @f$. """ return (cls._A, cls._B) @classmethod def is_singular(cls): """ Test whether the curve is singular or not: return @c True if, and only if, the curve is singular. A curve is singular if the polynomial @f$ x^3 + Ax + B @f$ has repeated roots. """ # A curve is singular if and only if its determinant is 0 # (in fields of characteristic neither 2 nor 3). return not 4 * cls._A**3 + 27 * cls._B**2