polynomial_evalrep.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. #| # Evaluation Representation of Polynomials and FFT optimizations
  2. #| In addition to the coefficient-based representation of polynomials used
  3. #| in babysnark.py, for performance we will also use an alternative
  4. #| representation where the polynomial is evaluated at a fixed set of points.
  5. #| Some operations, like multiplication and division, are significantly more
  6. #| efficient in this form.
  7. #| We can use FFT-based tools for efficiently converting
  8. #| between coefficient and evaluation representation.
  9. #|
  10. #| This library provides:
  11. #| - Fast fourier transform for finite fields
  12. #| - Interpolation and evaluation using FFT
  13. from finite_fields.finitefield import FiniteField
  14. from finite_fields.polynomial import polynomialsOver
  15. from finite_fields.euclidean import extendedEuclideanAlgorithm
  16. import random
  17. from finite_fields.numbertype import typecheck, memoize, DomainElement
  18. from functools import reduce
  19. import numpy as np
  20. #| ## Fast Fourier Transform on Finite Fields
  21. def fft_helper(a, omega, field):
  22. """
  23. Given coefficients A of polynomial this method does FFT and returns
  24. the evaluation of the polynomial at [omega^0, omega^(n-1)]
  25. If the polynomial is a0*x^0 + a1*x^1 + ... + an*x^n then the coefficients
  26. list is of the form [a0, a1, ... , an].
  27. """
  28. n = len(a)
  29. assert not (n & (n - 1)), "n must be a power of 2"
  30. if n == 1:
  31. return a
  32. b, c = a[0::2], a[1::2]
  33. b_bar = fft_helper(b, pow(omega, 2), field)
  34. c_bar = fft_helper(c, pow(omega, 2), field)
  35. a_bar = [field(1)] * (n)
  36. for j in range(n):
  37. k = j % (n // 2)
  38. a_bar[j] = b_bar[k] + pow(omega, j) * c_bar[k]
  39. return a_bar
  40. #| ## Representing a polynomial by evaluation at fixed points
  41. @memoize
  42. def make_polynomial_evalrep(field, omega, n):
  43. assert n & n - 1 == 0, "n must be a power of 2"
  44. # Check that omega is an n'th primitive root of unity
  45. assert type(omega) is field
  46. omega = field(omega)
  47. assert omega**(n) == 1
  48. _powers = [omega**i for i in range(n)]
  49. assert len(set(_powers)) == n
  50. _poly_coeff = polynomialsOver(field)
  51. class PolynomialEvalRep(object):
  52. def __init__(self, xs, ys):
  53. # Each element of xs must be a power of omega.
  54. # There must be a corresponding y for every x.
  55. if type(xs) is not tuple:
  56. xs = tuple(xs)
  57. if type(ys) is not tuple:
  58. ys = tuple(ys)
  59. assert len(xs) <= n+1
  60. assert len(xs) == len(ys)
  61. for x in xs:
  62. assert x in _powers
  63. for y in ys:
  64. assert type(y) is field
  65. self.evalmap = dict(zip(xs, ys))
  66. @classmethod
  67. def from_coeffs(cls, poly):
  68. assert type(poly) is _poly_coeff
  69. assert poly.degree() <= n
  70. padded_coeffs = poly.coefficients + [field(0)] * (n - len(poly.coefficients))
  71. ys = fft_helper(padded_coeffs, omega, field)
  72. xs = [omega**i for i in range(n) if ys[i] != 0]
  73. ys = [y for y in ys if y != 0]
  74. return cls(xs, ys)
  75. def to_coeffs(self):
  76. # To convert back to the coefficient form, we use polynomial interpolation.
  77. # The non-zero elements stored in self.evalmap, so we fill in the zero values
  78. # here.
  79. ys = [self.evalmap[x] if x in self.evalmap else field(0) for x in _powers]
  80. coeffs = [b / field(n) for b in fft_helper(ys, 1 / omega, field)]
  81. return _poly_coeff(coeffs)
  82. _lagrange_cache = {}
  83. def __call__(self, x):
  84. if type(x) is int:
  85. x = field(x)
  86. assert type(x) is field
  87. xs = _powers
  88. def lagrange(x, xi):
  89. # Let's cache lagrange values
  90. if (x,xi) in PolynomialEvalRep._lagrange_cache:
  91. return PolynomialEvalRep._lagrange_cache[(x,xi)]
  92. mul = lambda a,b: a*b
  93. num = reduce(mul, [x - xj for xj in xs if xj != xi], field(1))
  94. den = reduce(mul, [xi - xj for xj in xs if xj != xi], field(1))
  95. PolynomialEvalRep._lagrange_cache[(x,xi)] = num / den
  96. return PolynomialEvalRep._lagrange_cache[(x,xi)]
  97. y = field(0)
  98. for xi, yi in self.evalmap.items():
  99. y += yi * lagrange(x, xi)
  100. return y
  101. def __mul__(self, other):
  102. # Scale by integer
  103. if type(other) is int:
  104. other = field(other)
  105. if type(other) is field:
  106. return PolynomialEvalRep(self.evalmap.keys(),
  107. [other * y for y in self.evalmap.values()])
  108. # Multiply another polynomial in the same representation
  109. if type(other) is type(self):
  110. xs = []
  111. ys = []
  112. for x, y in self.evalmap.items():
  113. if x in other.evalmap:
  114. xs.append(x)
  115. ys.append(y * other.evalmap[x])
  116. return PolynomialEvalRep(xs, ys)
  117. @typecheck
  118. def __iadd__(self, other):
  119. # Add another polynomial to this one in place.
  120. # This is especially efficient when the other polynomial is sparse,
  121. # since we only need to add the non-zero elements.
  122. for x, y in other.evalmap.items():
  123. if x not in self.evalmap:
  124. self.evalmap[x] = y
  125. else:
  126. self.evalmap[x] += y
  127. return self
  128. @typecheck
  129. def __add__(self, other):
  130. res = PolynomialEvalRep(self.evalmap.keys(), self.evalmap.values())
  131. res += other
  132. return res
  133. def __sub__(self, other): return self + (-other)
  134. def __neg__(self): return PolynomialEvalRep(self.evalmap.keys(),
  135. [-y for y in self.evalmap.values()])
  136. def __truediv__(self, divisor):
  137. # Scale by integer
  138. if type(divisor) is int:
  139. other = field(divisor)
  140. if type(divisor) is field:
  141. return self * (1/divisor)
  142. if type(divisor) is type(self):
  143. res = PolynomialEvalRep((),())
  144. for x, y in self.evalmap.items():
  145. assert x in divisor.evalmap
  146. res.evalmap[x] = y / divisor.evalmap[x]
  147. return res
  148. return NotImplemented
  149. def __copy__(self):
  150. return PolynomialEvalRep(self.evalmap.keys(), self.evalmap.values())
  151. def __repr__(self):
  152. return f'PolyEvalRep[{hex(omega.n)[:15]}...,{n}]({len(self.evalmap)} elements)'
  153. @classmethod
  154. def divideWithCoset(cls, p, t, c=field(3)):
  155. """
  156. This assumes that p and t are polynomials in coefficient representation,
  157. and that p is divisible by t.
  158. This function is useful when t has roots at some or all of the powers of omega,
  159. in which case we cannot just convert to evalrep and use division above
  160. (since it would cause a divide by zero.
  161. Instead, we evaluate p(X) at powers of (c*omega) for some constant cofactor c.
  162. To do this efficiently, we create new polynomials, pc(X) = p(cX), tc(X) = t(cX),
  163. and evaluate these at powers of omega. This conversion can be done efficiently
  164. on the coefficient representation.
  165. See also: cosetFFT in libsnark / libfqfft.
  166. https://github.com/scipr-lab/libfqfft/blob/master/libfqfft/evaluation_domain/domains/extended_radix2_domain.tcc
  167. """
  168. assert type(p) is _poly_coeff
  169. assert type(t) is _poly_coeff
  170. # Compute p(cX), t(cX) by multiplying coefficients
  171. c_acc = field(1)
  172. pc = _poly_coeff(list(p.coefficients)) # make a copy
  173. for i in range(p.degree() + 1):
  174. pc.coefficients[-i-1] *= c_acc
  175. c_acc *= c
  176. c_acc = field(1)
  177. tc = _poly_coeff(list(t.coefficients)) # make a copy
  178. for i in range(t.degree() + 1):
  179. tc.coefficients[-i-1] *= c_acc
  180. c_acc *= c
  181. # Divide using evalrep
  182. pc_rep = cls.from_coeffs(pc)
  183. tc_rep = cls.from_coeffs(tc)
  184. hc_rep = pc_rep / tc_rep
  185. hc = hc_rep.to_coeffs()
  186. # Compute h(X) from h(cX) by dividing coefficients
  187. c_acc = field(1)
  188. h = _poly_coeff(list(hc.coefficients)) # make a copy
  189. for i in range(hc.degree() + 1):
  190. h.coefficients[-i-1] /= c_acc
  191. c_acc *= c
  192. # Correctness checks
  193. # assert pc == tc * hc
  194. # assert p == t * h
  195. return h
  196. return PolynomialEvalRep
  197. #| ## Sparse Matrix
  198. #| In our setting, we have O(m*m) elements in the matrix, and expect the number of
  199. #| elements to be O(m).
  200. #| In this setting, it's appropriate to use a rowdict representation - a dense
  201. #| array of dictionaries, one for each row, where the keys of each dictionary
  202. #| are column indices.
  203. class RowDictSparseMatrix():
  204. # Only a few necessary methods are included here.
  205. # This could be replaced with a generic sparse matrix class, such as scipy.sparse,
  206. # but this does not work as well with custom value types like Fp
  207. def __init__(self, m, n, zero=None):
  208. self.m = m
  209. self.n = n
  210. self.shape = (m,n)
  211. self.zero = zero
  212. self.rowdicts = [dict() for _ in range(m)]
  213. def __setitem__(self, key, v):
  214. i, j = key
  215. self.rowdicts[i][j] = v
  216. def __getitem__(self, key):
  217. i, j = key
  218. return self.rowdicts[i][j] if j in self.rowdicts[i] else self.zero
  219. def items(self):
  220. for i in range(self.m):
  221. for j, v in self.rowdicts[i].items():
  222. yield (i,j), v
  223. def dot(self, other):
  224. if isinstance(other, np.ndarray):
  225. assert other.dtype == 'O'
  226. assert other.shape in ((self.n,),(self.n,1))
  227. ret = np.empty((self.m,), dtype='O')
  228. ret.fill(self.zero)
  229. for i in range(self.m):
  230. for j, v in self.rowdicts[i].items():
  231. ret[i] += other[j] * v
  232. return ret
  233. def to_dense(self):
  234. mat = np.empty((self.m, self.n), dtype='O')
  235. mat.fill(self.zero)
  236. for (i,j), val in self.items():
  237. mat[i,j] = val
  238. return mat
  239. def __repr__(self): return repr(self.rowdicts)
  240. #-
  241. # Examples
  242. if __name__ == '__main__':
  243. import misc
  244. Fp = FiniteField(52435875175126190479447740508185965837690552500527637822603658699938581184513,1) # (# noqa: E501)
  245. Poly = polynomialsOver(Fp)
  246. n = 8
  247. omega = misc.get_omega(Fp, n)
  248. PolyEvalRep = make_polynomial_evalrep(Fp, omega, n)
  249. f = Poly([1,2,3,4,5])
  250. xs = tuple([omega**i for i in range(n)])
  251. ys = tuple(map(f, xs))
  252. # print('xs:', xs)
  253. # print('ys:', ys)
  254. assert f == PolyEvalRep(xs, ys).to_coeffs()