import logging from sage.all import sage_eval, PolynomialRing, QQ, SR, ceil, factorial, floor, log, matrix, prod, var load("sage/operators.sage") # Logger logger = logging.getLogger(__name__) logging.basicConfig(level=logging.DEBUG) class Period: """ A class representing a period as a formal power series in z-variables and their logs. The coefficients are stored in a dictionary of dictionaries, where the first key is the multi-index of the logarithmic part and the second key is the multi-index of the z-variables. So, for example, the coefficient of log(z0)^2 * log(z1) * z0^3 * z1^2 would be stored as coefficients[(2, 1)][(3, 2)] """ def _initialise_ring(self): z_names = ['z%d' % i for i in range(self.no_variables)] log_names = ['L%d' % i for i in range(self.no_variables)] ring = PolynomialRing(QQ, z_names + log_names) z_gens = ring.gens()[:self.no_variables] log_gens = ring.gens()[self.no_variables:] return ring, z_gens, log_gens # type: (PolynomialRing, list, list) def _period_from_string(self, period_string: str) -> dict: ring, z_gens, log_gens = self._initialise_ring() log_of_z = dict(zip(z_gens, log_gens)) def log(zi): try: return log_of_z[zi] except (KeyError, TypeError): raise ValueError( "log(...) may only be applied to one of the z-variables z0, ..., z%d" % (self.no_variables - 1) ) parse_locals = dict(ring.gens_dict()) parse_locals['log'] = log try: expression = ring(sage_eval(period_string, locals=parse_locals)) except Exception as e: raise ValueError("Invalid period string: %s" % e) coefficients = {} for coeff, monomial in expression: exponents = monomial.exponents()[0] z_index = tuple(exponents[:self.no_variables]) log_index = tuple(exponents[self.no_variables:]) coefficients.setdefault(log_index, {})[z_index] = coeff return coefficients def _period_to_string(self) -> str: ring, z_gens, log_gens = self._initialise_ring() expression = ring(0) for log_index, z_dict in self.coefficients.items(): for z_index, coeff in z_dict.items(): monomial = coeff for i in range(self.no_variables): monomial *= (log_gens[i] ** log_index[i]) * (z_gens[i] ** z_index[i]) expression += monomial log_substitutions = {log_gens[i]: log(SR(z_gens[i])) for i in range(self.no_variables)} result = SR(expression).subs(log_substitutions) # A nonzero indicial ρ_i means the coefficients above are for z_i^k, but the # actual solution is z_i^(ρ_i + k); make that explicit in the printed form. indicial_prefactor = prod( (SR(z_gens[i]) ** self.indicials[i] for i in range(self.no_variables) if self.indicials[i] != 0), SR(1), ) if indicial_prefactor != 1: result *= indicial_prefactor return str(result) def _max_z_degree(self) -> int: # Highest total z-degree (sum of the z-multi-index) among all coefficients. if not self.coefficients: return 0 return max( sum(z_index) for z_dict in self.coefficients.values() for z_index in z_dict ) def _truncate_coefficients(self, order: int) -> dict: # Drop every (log_index, z_index) entry whose total z-degree exceeds order, # removing it from the dictionary rather than merely zeroing it out. truncated = {} for log_index, z_dict in self.coefficients.items(): kept = {z_index: coeff for z_index, coeff in z_dict.items() if sum(z_index) <= order} if kept: truncated[log_index] = kept return truncated def _apply_theta(self, coefficients: dict, index: int) -> dict: """ Applies the logarithmic derivative theta_i = z_i * d/dz_i once to a coefficients dict of the same shape as self.coefficients. It uses the product rule theta_i(z^a log(z)^k) = a_i * z^a log(z)^k + k_i * z^a log(z)^(k - e_i). z_index entries are offsets from self.indicials: a stored z_index of a really means z^(a + self.indicials[index]), so theta_i's eigenvalue is a_i + self.indicials[index] rather than the bare a_i (self.indicials is all-zero unless explicitly given, in which case this reduces to the ordinary power-series rule). """ result = {} for log_index, z_dict in coefficients.items(): for z_index, coeff in z_dict.items(): a_i = z_index[index] + self.indicials[index] if a_i != 0: inner = result.setdefault(log_index, {}) inner[z_index] = inner.get(z_index, 0) + a_i * coeff k_i = log_index[index] if k_i != 0: lowered_log_index = log_index[:index] + (k_i - 1,) + log_index[index + 1:] inner = result.setdefault(lowered_log_index, {}) inner[z_index] = inner.get(z_index, 0) + k_i * coeff return result def apply_operator(self, pf_operator: PFOperator) -> "Period": """ Applies a PFOperator to this period and returns the result as a new Period, truncated to self.order. Each monomial of the operator is normalised as coeff * z^p * theta^q (see PFOperator's docstring), so theta^q is applied to the period first and the result is then multiplied by coeff * z^p. """ if pf_operator.no_variables != self.no_variables: raise ValueError( "Variable count mismatch: period has %d variable(s), operator has %d." % (self.no_variables, pf_operator.no_variables) ) result_coefficients = {} for monomial_coeff, monomial in pf_operator.operator: exponents = monomial.exponents()[0] z_exponents = exponents[:self.no_variables] theta_exponents = exponents[self.no_variables:] term = self.coefficients for i, power in enumerate(theta_exponents): for _ in range(power): term = self._apply_theta(term, i) for log_index, z_dict in term.items(): inner = result_coefficients.setdefault(log_index, {}) for z_index, coeff in z_dict.items(): shifted_z_index = tuple(z_index[i] + z_exponents[i] for i in range(self.no_variables)) inner[shifted_z_index] = inner.get(shifted_z_index, 0) + monomial_coeff * coeff result_coefficients = { log_index: {z_index: c for z_index, c in z_dict.items() if c != 0} for log_index, z_dict in result_coefficients.items() } result_coefficients = {log_index: z_dict for log_index, z_dict in result_coefficients.items() if z_dict} return Period( no_variables=self.no_variables, coefficients=result_coefficients, order=self.order, indicials=self.indicials, ) def find_annihilating_operators(self, z_degree: int, theta_degree: int) -> list: """ Finds operators annihilating this period among PFOperator Ansätze of the given z- and theta-degree. """ ansatz = PFOperatorAnsatz(self.no_variables, theta_degree, z_degree) unknowns = ansatz.unknowns equations = [ coeff for z_dict in self.apply_operator(ansatz).coefficients.values() for coeff in z_dict.values() ] coefficient_rows = [[SR(equation).coefficient(b) for b in unknowns] for equation in equations] kernel_basis = matrix(QQ, coefficient_rows, ncols=len(unknowns)).right_kernel().basis() operators = [] for basis_vector in kernel_basis: solution = {unknowns[j]: basis_vector[j] for j in range(len(unknowns))} solved_operator = ansatz.operator.map_coefficients(lambda c: SR(c).subs(solution)) operators.append(PFOperator(str(solved_operator), no_variables=self.no_variables)) return operators def __init__( self, no_variables: int = 1, coefficients: dict = None, period_string: str = None, order: int = None, indicials: list = None, ): self.no_variables = no_variables # indicials[i] is the (rational) Frobenius exponent ρ_i of z_i: a stored # z-index of a really represents z^(a + indicials[i]). self.indicials = list(indicials) if indicials is not None else [0] * no_variables # Use coefficients or period_string to initialize the period if period_string is not None: if coefficients is not None: raise ValueError("Provide either coefficients or period_string, not both.") self.coefficients = self._period_from_string(period_string) self.period_string = period_string logger.debug("Using string for initialisation.") elif coefficients is None: self.coefficients = {} else: self.coefficients = coefficients self.period_string = self._period_to_string() logger.debug("Using coefficients for initialisation.") if order is None: self.order = self._max_z_degree() logger.debug("Order not provided, using maximum z-degree: %d", self.order) else: self.coefficients = self._truncate_coefficients(order) self.order = order self.period_string = self._period_to_string() logger.debug("Truncated coefficients to order %d.", self.order) logger.info("Initialised Period in %d variables at order %d.", self.no_variables, self.order) @classmethod def from_l_vectors(cls, l_vectors: list, order: int) -> "Period": """ Computes the holomorphic (fundamental) period directly from the GLSM/Mori charge vectors l^(1), ..., l^(m) of a toric Calabi-Yau (m = number of moduli), via the hypergeometric-type sum varpi_0(z) = sum_{a in Z_{>=0}^m} (prod_j Gamma(1 - l_num^(j).a)) / (prod_i Gamma(1 + l_den^(i).a)) * z^a, see e.g. Hosono-Klemm-Theisen-Yau. l_vectors is a list of the m charge vectors, each given as [numerator part, denominator part]: the components landing in the numerator resp. denominator Gamma-functions (for a Calabi-Yau hypersurface/CICY, these are the nef-partition weight(s) resp. the toric divisor charges). For m = 1 the outer list may be dropped, e.g. the quintic is given by [[-5], [1, 1, 1, 1, 1]], while P^4_{1,1,2,2,6}[12] is given by [[[-6], [3, 2, 1, 0, 0, 0]], [[0], [0, 0, -3, 1, 1, 1]]]. For a Calabi-Yau charge vector the numerator Gamma-functions never hit a pole, since their argument only grows as a grows; only the denominator factors can vanish - at a non-positive-integer argument - which is exactly what truncates each a_k to a finite range once the others are fixed. This makes summing directly over that range far cheaper than solving the full Picard-Fuchs system term by term. """ # A single charge vector [numerator part, denominator part] has integer entries in its parts. if l_vectors and l_vectors[0] and not isinstance(l_vectors[0][0], (list, tuple)): l_vectors = [l_vectors] if any(len(l_vector) != 2 for l_vector in l_vectors): raise ValueError("Each charge vector must be given as [numerator part, denominator part].") m = len(l_vectors) numerator_charges = [l_vector[0] for l_vector in l_vectors] denominator_charges = [l_vector[1] for l_vector in l_vectors] len0 = len(numerator_charges[0]) if m else 0 len1 = len(denominator_charges[0]) if m else 0 # Each denominator Gamma-factor is linear in a; pivot it on the smallest index j with # a nonzero coefficient there. Since a is built up from a_{m-1} down to a_0, every # other index still appearing in that linear form is then already fixed, turning the # factor's pole at non-positive integers into a concrete bound on a_j. pivots = [[] for _ in range(m)] for i in range(len1): charges = [denominator_charges[k][i] for k in range(m)] j = next(k for k in range(m) if charges[k] != 0) pivots[j].append(charges) coefficients = {} a = [0] * m def recurse(j, num_partial, den_partial, degree): if j < 0: numerator = prod(factorial(-x) for x in num_partial) denominator = prod(factorial(x) for x in den_partial) coefficients[tuple(a)] = QQ(numerator) / QQ(denominator) return lower, upper = 0, order - degree for charges in pivots[j]: c = charges[j] bound = -QQ(sum(charges[k] * a[k] for k in range(j + 1, m))) / QQ(c) if c < 0: upper = min(upper, floor(bound)) else: lower = max(lower, ceil(bound)) for a_j in range(lower, upper + 1): a[j] = a_j recurse( j - 1, [num_partial[jj] + numerator_charges[j][jj] * a_j for jj in range(len0)], [den_partial[ii] + denominator_charges[j][ii] * a_j for ii in range(len1)], degree + a_j, ) recurse(m - 1, [0] * len0, [0] * len1, 0) logger.info("Computed holomorphic period from l-vectors for %d moduli at order %d.", m, order) return cls(no_variables=m, coefficients={tuple([0] * m): coefficients}, order=order) class PeriodAnsatz(Period): """ A class for period Ansätze, characterised by number of variables and their z- and log-multi-degrees. """ def __init__(self, no_variables: int, z_degree: int, log_degree: int, indicials: list = None): self.no_variables = no_variables self.z_degree = z_degree self.log_degree = log_degree log_indices = _multi_indices(self.no_variables, log_degree) z_indices = _multi_indices(self.no_variables, z_degree) # Every (log_index, z_index) pair allowed by the degree bounds gets its own # fresh unknown, to be solved for once a PFOperator is applied to the Ansatz. coefficients = { log_index: { z_index: var("a_" + "_".join(str(x) for x in log_index + z_index)) for z_index in z_indices } for log_index in log_indices } super().__init__(no_variables=no_variables, coefficients=coefficients, order=z_degree, indicials=indicials) self.expansion_coefficients = self.coefficients logger.info( "Initialised PeriodAnsatz with %d unknown coefficient(s).", sum(len(z_dict) for z_dict in self.expansion_coefficients.values()), )