def riemanntheta_high_dim(X, Yinv, T, z, g, rad, max_points = 10000000): parRiemann = RiemannThetaCuda(1,512) #initialize parRiemann parRiemann.compile(g) parRiemann.cache_omega_real(X) parRiemann.cache_omega_imag(Yinv,T) #compile the box_points program point_finder = func1() R = get_rad(T, rad) print R num_int_points = (2*R + 1)**g num_partitions = num_int_points//max_points num_final_partition = num_int_points - num_partitions*max_points osc_part = 0 + 0*1.j if (num_partitions > 0): S = gpuarray.zeros(np.int(max_points * g), dtype=np.double) print "Required number of iterations" print num_partitions print for p in range(num_partitions): print p print S = box_points(point_finder, max_points*p, max_points*(p+1),g,R, S) parRiemann.cache_intpoints(S, gpu_already=True) osc_part += parRiemann.compute_v_without_derivs(np.array([z])) S = gpuarray.zeros(np.int((num_int_points - num_partitions*max_points)*g), dtype = np.double) print num_partitions*max_points,num_int_points S = box_points(point_finder, num_partitions*max_points, num_int_points, g, R,S) parRiemann.cache_intpoints(S,gpu_already = True) osc_part += parRiemann.compute_v_without_derivs(np.array([z])) print osc_part return osc_part
def riemanntheta_high_dim(X, Yinv, T, z, g, rad, max_points=10000000): parRiemann = RiemannThetaCuda(1, 512) #initialize parRiemann parRiemann.compile(g) parRiemann.cache_omega_real(X) parRiemann.cache_omega_imag(Yinv, T) #compile the box_points program point_finder = func1() R = get_rad(T, rad) print R num_int_points = (2 * R + 1)**g num_partitions = num_int_points // max_points num_final_partition = num_int_points - num_partitions * max_points osc_part = 0 + 0 * 1.j if (num_partitions > 0): S = gpuarray.zeros(np.int(max_points * g), dtype=np.double) print "Required number of iterations" print num_partitions print for p in range(num_partitions): print p print S = box_points(point_finder, max_points * p, max_points * (p + 1), g, R, S) parRiemann.cache_intpoints(S, gpu_already=True) osc_part += parRiemann.compute_v_without_derivs(np.array([z])) S = gpuarray.zeros(np.int( (num_int_points - num_partitions * max_points) * g), dtype=np.double) print num_partitions * max_points, num_int_points S = box_points(point_finder, num_partitions * max_points, num_int_points, g, R, S) parRiemann.cache_intpoints(S, gpu_already=True) osc_part += parRiemann.compute_v_without_derivs(np.array([z])) print osc_part return osc_part
class RiemannTheta_Function(object): r""" Creates an instance of the Riemann theta function parameterized by a Riemann matrix ``Omega``, directional derivative ``derivs``, and derivative evaluation accuracy radius. See module level documentation for more information about the Riemann theta function. The Riemann theta function `\theta : \CC^g \times H_g \to \CC` is defined by the infinite series .. math:: \theta( z | \Omega ) = \sum_{ n \in \ZZ^g } e^{ 2 \pi i \left( \tfrac{1}{2} \langle \Omega n, n \rangle + \langle z, n \rangle \right) } The precision of Riemann theta function evaluation is determined by the precision of the base ring. As shown in [CRTF], `n` th order derivatives introduce polynomial growth in the oscillatory part of the Riemann theta approximations thus making a global approximation formula impossible. Therefore, one must specify a ``deriv_accuracy_radius`` of guaranteed accuracy when computing derivatives of `\theta(z | \Omega)`. INPUT: - ``Omega`` -- a Riemann matrix (symmetric with positive definite imaginary part) - ``deriv`` -- (default: ``[]``) a list of `g`-tuples representing a directional derivative of `\theta`. A list of `n` lists represents an `n`th order derivative. - ``uniform`` -- (default: ``True``) a unform approximation allows the accurate computation of the Riemann theta function without having to recompute the integer points over which to take the finite sum. See [CRTF] for a more in-depth definition. - ``deriv_accuracy_radius`` -- (default: 5) the guaranteed radius of accuracy in computing derivatives of theta. This parameter is necessary due to the polynomial growth of the non-doubly exponential part of theta OUTPUT: - ``Function_RiemannTheta`` -- a Riemann theta function parameterized by the Riemann matrix `\Omega`, derivatives ``deriv``, whether or not to use a uniform approximation, and derivative accuracy radius ``deriv_accuracy_radius``. .. note:: For now, only second order derivatives are implemented. Approximation formulas are derived in [CRTF]. It is not exactly clear how to generalize these formulas. In most applications, second order derivatives are suficient. """ def __init__(self, uniform=True, deriv_accuracy_radius=5, tileheight = 32, tilewidth = 16): """ Defines parameters in constructed class instance. """ self.uniform = uniform self.deriv_accuracy_radius = deriv_accuracy_radius # cache radii, intpoints, and inverses self._rad = None self._intpoints = None self._Omega = None self._Yinv = None self._T = None self._Tinv = None self._prec = 1e-8 if (gpu_capable): self.parRiemann = RiemannThetaCuda(tileheight, tilewidth) def lattice(self): r""" Compute the complex lattice corresponding to the Riemann matix. .. note:: Not yet implemented. """ raise NotImplementedError() def genus(self): r""" The genus of the algebraic curve from which the Riemann matrix is calculated. If $\Omega$ is not block decomposable then this is just the dimension of the matrix. .. note:: Block decomposablility detection is difficult and not yet implemented. Currently, ``self.genus()`` just returns the size of the matrix. """ return NotImplementedError() def find_int_points(self,g, c, R, T,start): r""" Recursive helper function for computing the integer points needed in each coordinate direction. INPUT: - ``g`` -- the genus. recursively used to determine integer points along each axis. - ``c`` -- center of integer point computation. `0 \in \CC^g` is used when using the uniform approximation. - ``R`` -- the radius of the ellipsoid along the current axis. - ``start`` -- the starting integer point for each recursion along each axis. OUTPUT: - ``intpoints`` -- (list) a list of all of the integer points inside the bounding ellipsoid along a single axis ... todo:: Recursion can be memory intensive in Python. For genus `g<30` this is a reasonable computation but can be sped up by writing a loop instead. """ a_ = c[g] - R/(np.sqrt(np.pi)*T[g,g]) b_ = c[g] + R/(np.sqrt(np.pi)*T[g,g]) a = np.ceil(a_) b = np.floor(b_) # check if we reached the edge of the ellipsoid if not a <= b: return np.array([]) # last dimension reached: append points if g == 0: points = np.array([]) for i in range(a, b+1): #Note that this algorithm works backwards on the coordinates, #the last coordinate found is x1 if our coordinates are {x1,x2, ... xn} points = np.append(np.append([i],start), points) return points # # compute new shifts, radii, start, and recurse # newg = g-1 newT = T[:(newg+1),:(newg+1)] newTinv = la.inv(newT) pts = [] for n in range(a, b+1): chat = c[:newg+1] that = T[:newg+1,g] newc = (chat.T - (np.dot(newTinv, that)*(n - c[g]))).T newR = np.sqrt(R**2 - np.pi*(T[g,g] * (n - c[g]))**2) # XXX newstart = np.append([n],start) newpts = self.find_int_points(newg,newc,newR,newT,newstart) pts = np.append(pts,newpts) return pts def integer_points(self, Yinv, T, z, g, R): """ The set, `U_R`, of the integral points needed to compute Riemann theta at the complex point $z$ to the numerical precision given by the Riemann matirix base field precision. The set `U_R` of [CRTF], (21). .. math:: \left\{ n \in \ZZ^g : \pi ( n - c )^{t} \cdot Y \cdot (n - c ) < R^2, |c_j| < 1/2, j=1,\ldots,g \right\} Since `Y` is positive definite it has Cholesky decomposition `Y = T^t T`. Letting `\Lambda` be the lattice of vectors `v(n), n \in ZZ^g` of the form `v(n)=\sqrt{\pi} T (n + [[ Y^{-1} n]])`, we have that .. math:: S_R = \left\{ v(n) \in \Lambda : || v(n) || < R \right\} . Note that since the integer points are only required for oscillatory part of Riemann theta all over these points are near the point `0 \in \CC^g`. Additionally, if ``uniform == True`` then the set of integer points is independent of the input points `z \in \CC^g`. .. note:: To actually compute `U_R` one needs to compute the convex hull of `2^{g}` bounding ellipsoids. Since this is computationally expensive, an ellipsoid centered at `0 \in \CC^g` with large radius is computed instead. This can cause accuracy issues with ill-conditioned Riemann matrices, that is, those that produce long and narrow bounding ellipsoies. See [CRTF] Section ### for more information. INPUTS: - ``Yinv`` -- the inverse of the imaginary part of the Riemann matrix `\Omega` - ``T`` -- the Cholesky decomposition of the imaginary part of the Riemann matrix `\Omega` - ``z`` -- the point `z \in \CC` at which to compute `\theta(z|\Omega)` - ``R`` -- the first ellipsoid semi-axis length as computed by ``self.radius()`` """ # g = Yinv.shape[0] pi = np.pi z = np.array(z).reshape((g,1)) x = z.real y = z.imag # determine center of ellipsoid. if self.uniform: c = np.zeros((g,1)) intc = np.zeros((g,1)) leftc = np.zeros((g,1)) else: c = Yinv * y intc = c.round() leftc = c - intc int_points = self.find_int_points(g-1,leftc,R,T,[]) return int_points def radius(self, T, prec, deriv=[]): r""" Calculate the radius `R` to compute the value of the theta function to within `2^{-P + 1}` bits of precision where `P` is the real / complex precision given by the input matrix. Used primarily by ``RiemannTheta.integer_points()``. `R` is the radius of [CRTF] Theorems 2, 4, and 6. Input ----- - ``T`` -- the Cholesky decomposition of the imaginary part of the Riemann matrix `\Omega` - ``prec`` -- the desired precision of the computation - ``deriv`` -- (list) (default=``[]``) the derivative, if given. Radius increases as order of derivative increases. """ Pi = np.pi I = 1.0j g = np.float64(T.shape[0]) # compute the length of the shortest lattice vector #U = qflll(T) A = lattice_reduce(T) r = min(la.norm(A[:,i]) for i in range(int(g))) normTinv = la.norm(la.inv(T)) # solve for the radius using: # * Theorem 3 of [CRTF] (no derivative) # * Theorem 5 of [CRTF] (first order derivative) # * Theorem 7 of [CRTF] (second order derivative if len(deriv) == 0: eps = prec lhs = eps * (2.0/g) * (r/2.0)**g * gamma(g/2.0) ins = gammainccinv(g/2.0,lhs) R = np.sqrt(ins) + r/2.0 rad = max( R, (np.sqrt(2*g)+r)/2.0) elif len(deriv) == 1: # solve for left-hand side L = self.deriv_accuracy_radius normderiv = la.norm(np.array(deriv[0])) eps = prec lhs = (eps * (r/2.0)**g) / (np.sqrt(Pi)*g*normderiv*normTinv) # define right-hand-side function involving the incomplete gamma # function def rhs(ins): """ Right-hand side function for computing the bounding ellipsoid radius given a desired maximum error bound for the first derivative of the Riemann theta function. """ return gamma((g+1)/2)*gammaincc((g+1)/2, ins) + \ np.sqrt(Pi)*normTinv*L * gamma(g/2)*gammaincc(g/2, ins) - \ float(lhs) # define lower bound (guess) and attempt to solve for the radius lbnd = np.sqrt(g+2 + np.sqrt(g**2+8)) + r try: ins = fsolve(rhs, float(lbnd))[0] except RuntimeWarning: # fsolve had trouble finding the solution. We try # a larger initial guess since the radius increases # as desired precision increases try: ins = fsolve(rhs, float(2*lbnd))[0] except RuntimeWarning: raise ValueError, "Could not find an accurate bound for the radius. Consider using higher precision." # solve for radius R = np.sqrt(ins) + r/2.0 rad = max(R,lbnd) elif len(deriv) == 2: # solve for left-hand side L = self.deriv_accuracy_radius prodnormderiv = np.prod([la.norm(d) for d in deriv]) eps = prec lhs = (eps*(r/2.0)**g) / (2*Pi*g*prodnormderiv*normTinv**2) # define right-hand-side function involving the incomplete gamma # function def rhs(ins): """ Right-hand side function for computing the bounding ellipsoid radius given a desired maximum error bound for the second derivative of the Riemann theta function. """ return gamma((g+2)/2)*gammaincc((g+2)/2, ins) + \ 2*np.sqrt(Pi)*normTinv*L * \ gamma((g+1)/2)*gammaincc((g+1)/2,ins) + \ Pi*normTinv**2*L**2 * \ gamma(g/2)*gammaincc(g/2,ins) - float(lhs) # define lower bound (guess) and attempt to solve for the radius lbnd = np.sqrt(g+4 + np.sqrt(g**2+16)) + r try: ins = fsolve(rhs, float(lbnd))[0] except RuntimeWarning: # fsolve had trouble finding the solution. We try # a larger initial guess since the radius increases # as desired precision increases try: ins = fsolve(rhs, float(2*lbnd))[0] except RuntimeWarning: raise ValueError, "Could not find an accurate bound for the radius. Consider using higher precision." # solve for radius R = np.sqrt(ins) + r/2.0 rad = max(R,lbnd) else: # can't computer higher derivatives, yet raise NotImplementedError("Ellipsoid radius for first and second derivatives not yet implemented.") return rad """ Performs simple re-cacheing of matrices, also prepares them for gpu for processing if necessary. Input ----- Omega - the Riemann matrix X - The real part of Omega Y - The imaginary part of Omega Yinv - The inverse of Y T - The Cholesky Decomposition of Y g - The genus of the Riemann theta function prec - The desired precision deriv - the set of derivatives to compute (Possibly an empty set) Tinv - The inverse of T Output ----- Data structures ready for GPU computation. """ def recache(self, Omega, X, Y, Yinv, T, g, prec, deriv, Tinv, batch): recache_omega = not np.array_equal(self._Omega, Omega) recache_prec = self._prec != prec #Check if we've already computed the uniform radius and intpoints for this Omega/Precision if (recache_omega or recache_prec): #If not recompute the integer summation set. self._prec = prec self._rad = self.radius(T, prec, deriv=deriv) origin = [0]*g self._intpoints = self.integer_points(Yinv, T, origin, g, self._rad) #If gpu_capable is set to true and batch is set to true then the data structures need to #be loaded onto the GPU for computation. This code loads them onto the GPU and compiles #the pyCuda functions. if (gpu_capable and batch): self.parRiemann.cache_intpoints(self._intpoints) #Check if the gpu functions depending on the genus and Omega need to be compiled/recompiled if (self._Omega is None or not g == self._Omega.shape[0] or self.parRiemann.g is None): self.parRiemann.compile(g) self.parRiemann.cache_omega_real(X) self.parRiemann.cache_omega_imag(Yinv, T) #Check if the gpu functions depending only on Omega need to be recompiled else: #Check if the gpu functions depending on the real part of Omega need to be recompiled if (not np.array_equal(self._Omega.real, Omega.real)): self.parRiemann.cache_omega_real(X) #Check if the gpu functions depending on the imaginary part of Omega need to be recompiled if (not np.array_equal(self._Omega.imag, Omega.imag)): self.parRiemann.cache_omega_imag(Yinv, T) self._Omega = Omega """ Handles calls to the GPU. Input ----- Z - the set of points to compute theta(z, Omega) at. deriv - The derivatives to compute (possibly an empty list) gpu_max - The maximum number of points to compute on the GPU at once length - The number of points we're computing. (ie. length == |Z|) Output ------- u - A list of the exponential growth terms of theta (or deriv(theta)) for each z in Z v - A list of the approximations of the infite sum of theta (or deriv(theta)) for each z in Z """ def gpu_process(self, Z, deriv, gpu_max, length): v = np.array([]) u = np.array([]) #divide the set z into as many partitions as necessary num_partitions = (length-1)//(gpu_max) + 1 for i in range(0, num_partitions): #determine the starting and stopping points of the partition p_start = (i)*gpu_max p_stop = min(length, (i+1)*gpu_max) if (len(deriv) > 0): v_p = self.parRiemann.compute_v_with_derivs(Z[p_start: p_stop, :], deriv) else: v_p = self.parRiemann.compute_v_without_derivs(Z[p_start: p_stop, :]) u_p = self.parRiemann.compute_u() u = np.concatenate((u, u_p)) v = np.concatenate((v, v_p)) return u,v """ Computes the exponential and oscillatory part of the Riemann theta function. Or the directional derivative of theta. Input ----- z - The point (or set of points) to compute the Riemann Theta function at. Note that if z is a set of points the variable "batch" must be set to true. If z is a single point itshould be in the form of a 1-d numpy array, if z is a set of points it should be a list or 1-d numpy array of 1-d numpy arrays. Omega - The Riemann matrix batch - A variable that indicates whether or not a batch of points is being computed. prec - The desired digits of precision to compute theta up to. Note that precision is limited to double precision which is about ~15 decimal points. gpu - Indicates whether or not to do batch computations on a GPU, the default is set to yes if the proper pyCuda libraries are installed and no otherwise. gpu_max - The maximum number of points to be computed on a GPU at once. Output ------ u - A list of the exponential growth terms of theta (or deriv(theta)) for each z in Z v - A list of the approximations of the infite sum of theta (or deriv(theta)) for each z in Z """ def exp_and_osc_at_point(self, z, Omega, batch = False, prec=1e-12, deriv=[], gpu=gpu_capable, gpu_max = 500000): g = Omega.shape[0] pi = np.pi #Process all of the matrices into numpy matrices X = np.array(Omega.real) Y = np.array(Omega.imag) Yinv = np.array(la.inv(Y)) T = np.array(la.cholesky(Y)) Tinv = np.array(la.inv(T)) deriv = np.array(deriv) #Do recacheing if necessary self.recache(Omega, X, Y, Yinv, T, g, prec, deriv, Tinv, batch) # extract real and imaginary parts of input z length = 1 if batch: length = len(z) z = np.array(z).reshape((length, g)) # compute integer points: check for uniform approximation if self.uniform: R = self._rad S = self._intpoints elif(batch): raise Exception("Can't compute pointwise approximation for multiple points at once.\nUse uniform approximation or call the function seperately for each point.") else: R = self.radius(T, prec, deriv=deriv) S = self.integer_points(Yinv, T, Tinv, z, g, R) #Compute oscillatory and exponential terms if gpu and batch and (length > gpu_max): u,v = self.gpu_process(z, deriv, gpu_max, length) elif gpu and batch and len(deriv) > 0: v = self.parRiemann.compute_v_with_derivs(z, deriv) elif gpu and batch: v = self.parRiemann.compute_v_without_derivs(z) elif (len(deriv) > 0): v = riemanntheta_cy.finite_sum_derivatives(X, Yinv, T, z, S, deriv, g, batch) else: v = riemanntheta_cy.finite_sum(X, Yinv, T, z, S, g, batch) if (length > gpu_max and gpu): #u already computed pass elif (gpu and batch): u = self.parRiemann.compute_u() elif (batch): K = len(z) u = np.zeros(K) for i in range(K): w = np.array([z[i,:].imag]) val = np.pi*np.dot(w, np.dot(Yinv,w.T)).item(0,0) u[i] = val else: u = np.pi*np.dot(z.imag,np.dot(Yinv,z.imag.T)).item(0,0) return u,v """ TODO: Add documentation """ def characteristic(self, chars, z, Omega, deriv = [], prec=1e-8): val = 0 z = np.matrix(z).T alpha, beta = np.matrix(chars[0]).T, np.matrix(chars[1]).T z_tilde = z + np.dot(Omega,alpha) + beta if len(deriv) == 0: u,v = self.exp_and_osc_at_point(z_tilde, Omega) quadratic_term = np.dot(alpha.T, np.dot(Omega,alpha))[0,0] exp_shift = 2*np.pi*1.0j*(.5*quadratic_term + np.dot(alpha.T, (z + beta))) theta_val = np.exp(u + exp_shift)*v elif len(deriv) == 1: d = deriv[0] scalar_term = np.exp(2*np.pi*1.0j*(.5*np.dot(alpha.T, np.dot(Omega, alpha)) + np.dot(alpha.T, (z + beta)))) alpha_part = 2*np.pi*1.0j*alpha theta_eval = self.value_at_point(z_tilde, Omega, prec=prec) term1 = np.dot(theta_eval*alpha_part.T, d) term2 = self.value_at_point(z_tilde, Omega, prec=prec, deriv=d) theta_val = scalar_term*(term1 + term2) elif len(deriv) == 2: d1,d2 = np.matrix(deriv[0]).T, np.matrix(deriv[1]).T scalar_term = np.exp(2*np.pi*1.0j*(.5*np.dot(alpha.T, np.dot(Omega, alpha))[0,0] + np.dot(alpha.T, (z + beta))[0,0])) #Compute the non-theta hessian g = Omega.shape[0] non_theta_hess = np.zeros((g, g), dtype = np.complex128) theta_eval = self.value_at_point(z_tilde, Omega, prec=prec) theta_grad = np.zeros(g, dtype=np.complex128) for i in range(g): partial = np.zeros(g) partial[i] = 1.0 theta_grad[i] = self.value_at_point(z_tilde, Omega, prec = prec, deriv = partial) for n in xrange(g): for k in xrange(g): non_theta_hess[n,k] = 2*np.pi*1.j*alpha[k,0] * (2*np.pi*1.j*theta_eval*alpha[n,0] + theta_grad[n]) + (2*np.pi*1.j*theta_grad[k]*alpha[n,0]) term1 = np.dot(d1.T, np.dot(non_theta_hess, d2))[0,0] term2 = self.value_at_point(z_tilde, Omega, prec=prec, deriv=deriv) theta_val = scalar_term*(term1 + term2) else: return NotImplementedError() return theta_val r""" Returns the value of `\theta(z,\Omega)` at a point `z` or set of points if batch is True. """ def value_at_point(self, z, Omega, prec=1e-8, deriv=[], gpu=gpu_capable, batch=False): exp_part, osc_part = self.exp_and_osc_at_point(z, Omega, prec=prec, deriv=deriv, gpu=gpu,batch=batch) return np.exp(exp_part) * osc_part def __call__(self, z, Omega, prec=1e-8, deriv=[], gpu=gpu_capable, batch=False): r""" Returns the value of `\theta(z,\Omega)` at a point `z`. Lazy evaluation is done if the input contains symbolic variables. If batch is set to true then the functions expects a list/numpy array as input and returns a numpy array as output """ return self.value_at_point(z, Omega, prec=prec, deriv=deriv, gpu=gpu, batch=batch)
class RiemannTheta_Function(object): r""" Creates an instance of the Riemann theta function parameterized by a Riemann matrix ``Omega``, directional derivative ``derivs``, and derivative evaluation accuracy radius. See module level documentation for more information about the Riemann theta function. The Riemann theta function `\theta : \CC^g \times H_g \to \CC` is defined by the infinite series .. math:: \theta( z | \Omega ) = \sum_{ n \in \ZZ^g } e^{ 2 \pi i \left( \tfrac{1}{2} \langle \Omega n, n \rangle + \langle z, n \rangle \right) } The precision of Riemann theta function evaluation is determined by the precision of the base ring. As shown in [CRTF], `n` th order derivatives introduce polynomial growth in the oscillatory part of the Riemann theta approximations thus making a global approximation formula impossible. Therefore, one must specify a ``deriv_accuracy_radius`` of guaranteed accuracy when computing derivatives of `\theta(z | \Omega)`. INPUT: - ``Omega`` -- a Riemann matrix (symmetric with positive definite imaginary part) - ``deriv`` -- (default: ``[]``) a list of `g`-tuples representing a directional derivative of `\theta`. A list of `n` lists represents an `n`th order derivative. - ``uniform`` -- (default: ``True``) a unform approximation allows the accurate computation of the Riemann theta function without having to recompute the integer points over which to take the finite sum. See [CRTF] for a more in-depth definition. - ``deriv_accuracy_radius`` -- (default: 5) the guaranteed radius of accuracy in computing derivatives of theta. This parameter is necessary due to the polynomial growth of the non-doubly exponential part of theta OUTPUT: - ``Function_RiemannTheta`` -- a Riemann theta function parameterized by the Riemann matrix `\Omega`, derivatives ``deriv``, whether or not to use a uniform approximation, and derivative accuracy radius ``deriv_accuracy_radius``. .. note:: For now, only second order derivatives are implemented. Approximation formulas are derived in [CRTF]. It is not exactly clear how to generalize these formulas. In most applications, second order derivatives are suficient. """ def __init__(self, uniform=True, deriv_accuracy_radius=5, tileheight = 32, tilewidth = 16): """ Defines parameters in constructed class instance. """ self.uniform = uniform self.deriv_accuracy_radius = deriv_accuracy_radius # cache radii, intpoints, and inverses self._rad = None self._intpoints = None self._Omega = None self._Yinv = None self._T = None self._Tinv = None self._prec = 1e-8 if (gpu_capable): self.parRiemann = RiemannThetaCuda(tileheight, tilewidth) def lattice(self): r""" Compute the complex lattice corresponding to the Riemann matix. .. note:: Not yet implemented. """ raise NotImplementedError() def genus(self): r""" The genus of the algebraic curve from which the Riemann matrix is calculated. If $\Omega$ is not block decomposable then this is just the dimension of the matrix. .. note:: Block decomposablility detection is difficult and not yet implemented. Currently, ``self.genus()`` just returns the size of the matrix. """ return NotImplementedError() def find_int_points(self,g, c, R, T,start): r""" Recursive helper function for computing the integer points needed in each coordinate direction. INPUT: - ``g`` -- the genus. recursively used to determine integer points along each axis. - ``c`` -- center of integer point computation. `0 \in \CC^g` is used when using the uniform approximation. - ``R`` -- the radius of the ellipsoid along the current axis. - ``start`` -- the starting integer point for each recursion along each axis. OUTPUT: - ``intpoints`` -- (list) a list of all of the integer points inside the bounding ellipsoid along a single axis ... todo:: Recursion can be memory intensive in Python. For genus `g<30` this is a reasonable computation but can be sped up by writing a loop instead. """ a_ = c[g] - R/(np.sqrt(np.pi)*T[g,g]) b_ = c[g] + R/(np.sqrt(np.pi)*T[g,g]) a = np.ceil(a_) b = np.floor(b_) # check if we reached the edge of the ellipsoid if not a <= b: return np.array([]) # last dimension reached: append points if g == 0: points = np.array([]) for i in range(a, b+1): #Note that this algorithm works backwards on the coordinates, #the last coordinate found is x1 if our coordinates are {x1,x2, ... xn} points = np.append(np.append([i],start), points) return points # # compute new shifts, radii, start, and recurse # newg = g-1 newT = T[:(newg+1),:(newg+1)] newTinv = la.inv(newT) pts = [] for n in range(a, b+1): chat = c[:newg+1] that = T[:newg+1,g] newc = (chat.T - (np.dot(newTinv, that)*(n - c[g]))).T newR = np.sqrt(R**2 - np.pi*(T[g,g] * (n - c[g]))**2) # XXX newstart = np.append([n],start) newpts = self.find_int_points(newg,newc,newR,newT,newstart) pts = np.append(pts,newpts) return pts def integer_points(self, Yinv, T, z, g, R): """ The set, `U_R`, of the integral points needed to compute Riemann theta at the complex point $z$ to the numerical precision given by the Riemann matirix base field precision. The set `U_R` of [CRTF], (21). .. math:: \left\{ n \in \ZZ^g : \pi ( n - c )^{t} \cdot Y \cdot (n - c ) < R^2, |c_j| < 1/2, j=1,\ldots,g \right\} Since `Y` is positive definite it has Cholesky decomposition `Y = T^t T`. Letting `\Lambda` be the lattice of vectors `v(n), n \in ZZ^g` of the form `v(n)=\sqrt{\pi} T (n + [[ Y^{-1} n]])`, we have that .. math:: S_R = \left\{ v(n) \in \Lambda : || v(n) || < R \right\} . Note that since the integer points are only required for oscillatory part of Riemann theta all over these points are near the point `0 \in \CC^g`. Additionally, if ``uniform == True`` then the set of integer points is independent of the input points `z \in \CC^g`. .. note:: To actually compute `U_R` one needs to compute the convex hull of `2^{g}` bounding ellipsoids. Since this is computationally expensive, an ellipsoid centered at `0 \in \CC^g` with large radius is computed instead. This can cause accuracy issues with ill-conditioned Riemann matrices, that is, those that produce long and narrow bounding ellipsoies. See [CRTF] Section ### for more information. INPUTS: - ``Yinv`` -- the inverse of the imaginary part of the Riemann matrix `\Omega` - ``T`` -- the Cholesky decomposition of the imaginary part of the Riemann matrix `\Omega` - ``z`` -- the point `z \in \CC` at which to compute `\theta(z|\Omega)` - ``R`` -- the first ellipsoid semi-axis length as computed by ``self.radius()`` """ # g = Yinv.shape[0] pi = np.pi z = np.array(z).reshape((g,1)) x = z.real y = z.imag # determine center of ellipsoid. if self.uniform: c = np.zeros((g,1)) intc = np.zeros((g,1)) leftc = np.zeros((g,1)) else: c = Yinv * y intc = c.round() leftc = c - intc int_points = self.find_int_points(g-1,leftc,R,T,[]) return int_points def radius(self, T, prec, deriv=[]): r""" Calculate the radius `R` to compute the value of the theta function to within `2^{-P + 1}` bits of precision where `P` is the real / complex precision given by the input matrix. Used primarily by ``RiemannTheta.integer_points()``. `R` is the radius of [CRTF] Theorems 2, 4, and 6. Input ----- - ``T`` -- the Cholesky decomposition of the imaginary part of the Riemann matrix `\Omega` - ``prec`` -- the desired precision of the computation - ``deriv`` -- (list) (default=``[]``) the derivative, if given. Radius increases as order of derivative increases. """ Pi = np.pi I = 1.0j g = np.float64(T.shape[0]) # compute the length of the shortest lattice vector #U = qflll(T) A = lattice_reduce(T) r = min(la.norm(A[:,i]) for i in range(int(g))) normTinv = la.norm(la.inv(T)) # solve for the radius using: # * Theorem 3 of [CRTF] (no derivative) # * Theorem 5 of [CRTF] (first order derivative) # * Theorem 7 of [CRTF] (second order derivative) if len(deriv) == 0: eps = prec lhs = eps * (2.0/g) * (r/2.0)**g * gamma(g/2.0) ins = gammainccinv(g/2.0,lhs) R = np.sqrt(ins) + r/2.0 rad = max( R, (np.sqrt(2*g)+r)/2.0) elif len(deriv) == 1: # solve for left-hand side L = self.deriv_accuracy_radius normderiv = la.norm(np.array(deriv[0])) eps = prec lhs = (eps * (r/2.0)**g) / (np.sqrt(Pi)*g*normderiv*normTinv) # define right-hand-side function involving the incomplete gamma # function def rhs(ins): """ Right-hand side function for computing the bounding ellipsoid radius given a desired maximum error bound for the first derivative of the Riemann theta function. """ return gamma((g+1)/2)*gammaincc((g+1)/2, ins) + \ np.sqrt(Pi)*normTinv*L * gamma(g/2)*gammaincc(g/2, ins) - \ float(lhs) # define lower bound (guess) and attempt to solve for the radius lbnd = np.sqrt(g+2 + np.sqrt(g**2+8)) + r try: ins = fsolve(rhs, float(lbnd))[0] except RuntimeWarning: # fsolve had trouble finding the solution. We try # a larger initial guess since the radius increases # as desired precision increases try: ins = fsolve(rhs, float(2*lbnd))[0] except RuntimeWarning: raise ValueError, "Could not find an accurate bound for the radius. Consider using higher precision." # solve for radius R = np.sqrt(ins) + r/2.0 rad = max(R,lbnd) elif len(deriv) == 2: # solve for left-hand side L = self.deriv_accuracy_radius prodnormderiv = np.prod([la.norm(d) for d in deriv]) eps = prec lhs = (eps*(r/2.0)**g) / (2*Pi*g*prodnormderiv*normTinv**2) # define right-hand-side function involving the incomplete gamma # function def rhs(ins): """ Right-hand side function for computing the bounding ellipsoid radius given a desired maximum error bound for the second derivative of the Riemann theta function. """ return gamma((g+2)/2)*gammaincc((g+2)/2, ins) + \ 2*np.sqrt(Pi)*normTinv*L * \ gamma((g+1)/2)*gammaincc((g+1)/2,ins) + \ Pi*normTinv**2*L**2 * \ gamma(g/2)*gammaincc(g/2,ins) - float(lhs) # define lower bound (guess) and attempt to solve for the radius lbnd = np.sqrt(g+4 + np.sqrt(g**2+16)) + r try: ins = fsolve(rhs, float(lbnd))[0] except RuntimeWarning: # fsolve had trouble finding the solution. We try # a larger initial guess since the radius increases # as desired precision increases try: ins = fsolve(rhs, float(2*lbnd))[0] except RuntimeWarning: raise ValueError, "Could not find an accurate bound for the radius. Consider using higher precision." # solve for radius R = np.sqrt(ins) + r/2.0 rad = max(R,lbnd) else: # can't computer higher derivatives, yet raise NotImplementedError("Ellipsoid radius for first and second derivatives not yet implemented.") return rad """ Performs simple re-cacheing of matrices, also prepares them for gpu for processing if necessary. Input ----- Omega - the Riemann matrix X - The real part of Omega Y - The imaginary part of Omega Yinv - The inverse of Y T - The Cholesky Decomposition of Y g - The genus of the Riemann theta function prec - The desired precision deriv - the set of derivatives to compute (Possibly an empty set) Tinv - The inverse of T Output ----- Data structures ready for GPU computation. """ def recache(self, Omega, X, Y, Yinv, T, g, prec, deriv, Tinv, batch): recache_omega = not np.array_equal(self._Omega, Omega) recache_prec = self._prec != prec #Check if we've already computed the uniform radius and intpoints for this Omega/Precision if (recache_omega or recache_prec): #If not recompute the integer summation set. self._prec = prec self._rad = self.radius(T, prec, deriv=deriv) origin = [0]*g self._intpoints = self.integer_points(Yinv, T, origin, g, self._rad) #If gpu_capable is set to true and batch is set to true then the data structures need to #be loaded onto the GPU for computation. This code loads them onto the GPU and compiles #the pyCuda functions. if (gpu_capable and batch): self.parRiemann.cache_intpoints(self._intpoints) #Check if the gpu functions depending on the genus and Omega need to be compiled/recompiled if (self._Omega is None or not g == self._Omega.shape[0] or self.parRiemann.g is None): self.parRiemann.compile(g) self.parRiemann.cache_omega_real(X) self.parRiemann.cache_omega_imag(Yinv, T) #Check if the gpu functions depending only on Omega need to be recompiled else: #Check if the gpu functions depending on the real part of Omega need to be recompiled if (not np.array_equal(self._Omega.real, Omega.real)): self.parRiemann.cache_omega_real(X) #Check if the gpu functions depending on the imaginary part of Omega need to be recompiled if (not np.array_equal(self._Omega.imag, Omega.imag)): self.parRiemann.cache_omega_imag(Yinv, T) self._Omega = Omega """ Handles calls to the GPU. Input ----- Z - the set of points to compute theta(z, Omega) at. deriv - The derivatives to compute (possibly an empty list) gpu_max - The maximum number of points to compute on the GPU at once length - The number of points we're computing. (ie. length == |Z|) Output ------- u - A list of the exponential growth terms of theta (or deriv(theta)) for each z in Z v - A list of the approximations of the infite sum of theta (or deriv(theta)) for each z in Z """ def gpu_process(self, Z, deriv, gpu_max, length): v = np.array([]) u = np.array([]) #divide the set z into as many partitions as necessary num_partitions = (length-1)//(gpu_max) + 1 for i in range(0, num_partitions): #determine the starting and stopping points of the partition p_start = (i)*gpu_max p_stop = min(length, (i+1)*gpu_max) if (len(deriv) > 0): v_p = self.parRiemann.compute_v_with_derivs(Z[p_start: p_stop, :], deriv) else: v_p = self.parRiemann.compute_v_without_derivs(Z[p_start: p_stop, :]) u_p = self.parRiemann.compute_u() u = np.concatenate((u, u_p)) v = np.concatenate((v, v_p)) return u,v """ Computes the exponential and oscillatory part of the Riemann theta function. Or the directional derivative of theta. Input ----- z - The point (or set of points) to compute the Riemann Theta function at. Note that if z is a set of points the variable "batch" must be set to true. If z is a single point itshould be in the form of a 1-d numpy array, if z is a set of points it should be a list or 1-d numpy array of 1-d numpy arrays. Omega - The Riemann matrix batch - A variable that indicates whether or not a batch of points is being computed. prec - The desired digits of precision to compute theta up to. Note that precision is limited to double precision which is about ~15 decimal points. gpu - Indicates whether or not to do batch computations on a GPU, the default is set to yes if the proper pyCuda libraries are installed and no otherwise. gpu_max - The maximum number of points to be computed on a GPU at once. Output ------ u - A list of the exponential growth terms of theta (or deriv(theta)) for each z in Z v - A list of the approximations of the infite sum of theta (or deriv(theta)) for each z in Z """ def exp_and_osc_at_point(self, z, Omega, batch = False, prec=1e-12, deriv=[], gpu=gpu_capable, gpu_max = 500000): g = Omega.shape[0] pi = np.pi #Process all of the matrices into numpy matrices X = np.array(Omega.real) Y = np.array(Omega.imag) Yinv = np.array(la.inv(Y)) T = np.array(la.cholesky(Y)) Tinv = np.array(la.inv(T)) deriv = np.array(deriv) #Do recacheing if necessary self.recache(Omega, X, Y, Yinv, T, g, prec, deriv, Tinv, batch) # extract real and imaginary parts of input z length = 1 if batch: length = len(z) z = np.array(z).reshape((length, g)) # compute integer points: check for uniform approximation if self.uniform: R = self._rad S = self._intpoints elif(batch): raise Exception("Can't compute pointwise approximation for multiple points at once.\nUse uniform approximation or call the function seperately for each point.") else: R = self.radius(T, prec, deriv=deriv) S = self.integer_points(Yinv, T, Tinv, z, g, R) #Compute oscillatory and exponential terms if gpu and batch and (length > gpu_max): u,v = self.gpu_process(z, deriv, gpu_max, length) elif gpu and batch and len(deriv) > 0: v = self.parRiemann.compute_v_with_derivs(z, deriv) elif gpu and batch: v = self.parRiemann.compute_v_without_derivs(z) elif (len(deriv) > 0): v = riemanntheta_cy.finite_sum_derivatives(X, Yinv, T, z, S, deriv, g, batch) else: v = riemanntheta_cy.finite_sum(X, Yinv, T, z, S, g, batch) if (length > gpu_max and gpu): #u already computed pass elif (gpu and batch): u = self.parRiemann.compute_u() elif (batch): K = len(z) u = np.zeros(K) for i in range(K): w = np.array([z[i,:].imag]) val = np.pi*np.dot(w, np.dot(Yinv,w.T)).item(0,0) u[i] = val else: u = np.pi*np.dot(z.imag,np.dot(Yinv,z.imag.T)).item(0,0) return u,v def exponential_part(self, *args, **kwds): return self.exp_and_osc_at_point(*args, **kwds)[0] def oscillatory_part(self, *args, **kwds): return self.exp_and_osc_at_point(*args, **kwds)[1] """ TODO: Add documentation """ def characteristic(self, chars, z, Omega, deriv = [], prec=1e-8): val = 0 z = np.matrix(z).T alpha, beta = np.matrix(chars[0]).T, np.matrix(chars[1]).T z_tilde = z + np.dot(Omega,alpha) + beta if len(deriv) == 0: u,v = self.exp_and_osc_at_point(z_tilde, Omega) quadratic_term = np.dot(alpha.T, np.dot(Omega,alpha))[0,0] exp_shift = 2*np.pi*1.0j*(.5*quadratic_term + np.dot(alpha.T, (z + beta))) theta_val = np.exp(u + exp_shift)*v elif len(deriv) == 1: d = deriv[0] scalar_term = np.exp(2*np.pi*1.0j*(.5*np.dot(alpha.T, np.dot(Omega, alpha)) + np.dot(alpha.T, (z + beta)))) alpha_part = 2*np.pi*1.0j*alpha theta_eval = self.value_at_point(z_tilde, Omega, prec=prec) term1 = np.dot(theta_eval*alpha_part.T, d) term2 = self.value_at_point(z_tilde, Omega, prec=prec, deriv=d) theta_val = scalar_term*(term1 + term2) elif len(deriv) == 2: d1,d2 = np.matrix(deriv[0]).T, np.matrix(deriv[1]).T scalar_term = np.exp(2*np.pi*1.0j*(.5*np.dot(alpha.T, np.dot(Omega, alpha))[0,0] + np.dot(alpha.T, (z + beta))[0,0])) #Compute the non-theta hessian g = Omega.shape[0] non_theta_hess = np.zeros((g, g), dtype = np.complex128) theta_eval = self.value_at_point(z_tilde, Omega, prec=prec) theta_grad = np.zeros(g, dtype=np.complex128) for i in range(g): partial = np.zeros(g) partial[i] = 1.0 theta_grad[i] = self.value_at_point(z_tilde, Omega, prec = prec, deriv = partial) for n in xrange(g): for k in xrange(g): non_theta_hess[n,k] = 2*np.pi*1.j*alpha[k,0] * (2*np.pi*1.j*theta_eval*alpha[n,0] + theta_grad[n]) + (2*np.pi*1.j*theta_grad[k]*alpha[n,0]) term1 = np.dot(d1.T, np.dot(non_theta_hess, d2))[0,0] term2 = self.value_at_point(z_tilde, Omega, prec=prec, deriv=deriv) theta_val = scalar_term*(term1 + term2) else: return NotImplementedError() return theta_val r""" Returns the value of `\theta(z,\Omega)` at a point `z` or set of points if batch is True. """ def value_at_point(self, z, Omega, prec=1e-8, deriv=[], gpu=gpu_capable, batch=False): exp_part, osc_part = self.exp_and_osc_at_point(z, Omega, prec=prec, deriv=deriv, gpu=gpu,batch=batch) return np.exp(exp_part) * osc_part def __call__(self, z, Omega, prec=1e-8, deriv=[], gpu=gpu_capable, batch=False): r""" Returns the value of `\theta(z,\Omega)` at a point `z`. Lazy evaluation is done if the input contains symbolic variables. If batch is set to true then the functions expects a list/numpy array as input and returns a numpy array as output """ return self.value_at_point(z, Omega, prec=prec, deriv=deriv, gpu=gpu, batch=batch)