import logging import os import pytest from sage.all import * # noqa: F401 load(os.path.join(os.path.dirname(__file__), "..", "sage", "periods.sage")) def _quintic_ideal(): # The quintic's Picard-Fuchs operator, of rank 4 with log structure [1, 1, 1, 1] at its MUM point. return PFIdeal( [PFOperator("theta0^4 - 5*z0*(5*theta0 + 1)*(5*theta0 + 2)*(5*theta0 + 3)*(5*theta0 + 4)", no_variables=1)], hodge_numbers=[1, 1, 1, 1], ) def _p22211_ideal(): # The Picard-Fuchs ideal of P_{22211}[8] at its MUM point (see # test_ideal_finds_power_series_solution_and_recovers_first_operator), with h^{2,1} = 2. D1 = PFOperator( "theta0^2*(theta0 - 2*theta1) - 4*z0*(4*theta0 + 3)*(4*theta0 + 2)*(4*theta0 + 1)", no_variables=2, ) D2 = PFOperator( "theta1^2 - z1*(2*theta1 - theta0 + 1)*(2*theta1 - theta0)", no_variables=2, ) return PFIdeal([D1, D2], hodge_numbers=[1, 2, 2, 1]) def _p333111_ideal(): # The Picard-Fuchs ideal of the fourfold P_{333111}[12] at its MUM point: the GKZ operators of # l^(1) = (-4; 1,1,1,0,0,0,1) and l^(2) = (0; 0,0,0,1,1,1,-3), the first without its left factor theta0. # With the intersection numbers kappa_0000 = 36, kappa_0001 = 12, kappa_0011 = 4 (and 0 else) that # toric_topdata computes, the vertical h^{2,2} is 3. D1 = PFOperator( "theta0^2*(theta0 - 3*theta1) - 4*z0*(4*theta0 + 1)*(4*theta0 + 2)*(4*theta0 + 3)", no_variables=2, ) D2 = PFOperator( "theta1^3 - z1*(theta0 - 3*theta1)*(theta0 - 3*theta1 - 1)*(theta0 - 3*theta1 - 2)", no_variables=2, ) return PFIdeal([D1, D2], hodge_numbers=[1, 2, 3, 2, 1]) def _starting_term(period): # A solution's lowest z-monomial (by total degree, then lexicographically) together with its coefficient # per log-monomial there, e.g. ((1,), {(1,): 1}) for z0*log(z0) + ... z_index = min( (z_index for z_dict in period.coefficients.values() for z_index, coeff in z_dict.items() if coeff != 0), key=lambda z_index: (sum(z_index), z_index), ) return z_index, { log_index: z_dict[z_index] for log_index, z_dict in period.coefficients.items() if z_dict[z_index] != 0 } def _nonzero(coefficients): # A coefficients dictionary without its zero entries, to compare bases that store zeros differently. return { log_index: {z_index: coeff for z_index, coeff in z_dict.items() if coeff != 0} for log_index, z_dict in coefficients.items() if any(coeff != 0 for coeff in z_dict.values()) } def _truncated(f, degree): # A polynomial without its terms of total degree above degree. return sum((coeff * monomial for coeff, monomial in f if monomial.degree() <= degree), f.parent()(0)) def _inverse_of_linear(constant, linear, degree): # 1/(constant + linear) for a linear form in eps, as a polynomial truncated at the given degree. return sum(((-linear / constant) ** j for j in range(degree + 1)), linear.parent()(0)) / constant def _classical_frobenius_periods(coefficient_ratio, starting_terms, order): # The classical Frobenius method. With coefficient_ratio(k) = c(k + eps)/c(eps) for the Gamma-function # coefficients c of the fundamental period (a polynomial in eps, truncated at the starting terms' degree), # the period starting with Q(log z) is Q(d/deps) applied to sum_k c(k + eps)/c(eps) * z^(k + eps) at eps = 0. n = starting_terms[0].parent().ngens() log_ring = PolynomialRing(QQ, n, ["L%d" % i for i in range(n)]) logs = log_ring.gens() periods = [{} for _ in starting_terms] for k in _multi_indices(n, order): ratio = coefficient_ratio(k) for period, starting_term in zip(periods, starting_terms): # d^alpha/deps^alpha of ratio * exp(eps . L) at eps = 0, summed over the monomials eps^alpha of Q value = log_ring(0) for q, q_monomial in starting_term: alpha = q_monomial.exponents()[0] for r, r_monomial in ratio: beta = r_monomial.exponents()[0] if all(beta[i] <= alpha[i] for i in range(n)): value += q * r * prod( factorial(alpha[i]) // factorial(alpha[i] - beta[i]) * logs[i] ** (alpha[i] - beta[i]) for i in range(n) ) for coeff, log_monomial in value: period.setdefault(tuple(log_monomial.exponents()[0]), {})[k] = coeff return periods def test_find_power_series_solution_recovers_quintic_period(): # The quintic's Picard-Fuchs operator has indicial equation theta0^4 = 0 at z0 = 0 # (a quadruple root at 0), so its holomorphic (non-logarithmic) power series solution # is found at indicial 0. Up to normalisation this is the classic quintic period # 1 + 120*z0 + 113400*z0^2 + ... op = PFOperator( "theta0^4 - 5*z0*(5*theta0 + 1)*(5*theta0 + 2)*(5*theta0 + 3)*(5*theta0 + 4)", no_variables=1, ) ideal = PFIdeal([op]) solutions = ideal.find_power_series_solution(indicials=[0], order=3) assert len(solutions) == 1 assert solutions[0].coefficients == {(0,): {(0,): 1, (1,): 120, (2,): 113400, (3,): 168168000}} def test_find_power_series_solution_handles_rational_indicial(): # theta0 - 1/2 kills z0^(1/2 + k) only when k = 0, since its eigenvalue is 1/2 + k; # so at indicial 1/2 there is exactly one solution (a constant multiple of sqrt(z0)), # while at indicial 0 no power series solution exists at all. op = PFOperator("theta0 - 1/2", no_variables=1) ideal = PFIdeal([op]) solutions = ideal.find_power_series_solution(indicials=[1/2], order=3) assert len(solutions) == 1 assert solutions[0].coefficients == {(0,): {(0,): 1, (1,): 0, (2,): 0, (3,): 0}} assert solutions[0].period_string == "sqrt(z0)" assert ideal.find_power_series_solution(indicials=[0], order=3) == [] def test_ideal_finds_power_series_solution_and_recovers_first_operator(): # Picard--Fuchs ideal for P_{22211}[8] at the MUM point: # D1 = Theta_x^2*(Theta_x - 2*Theta_y) - 4*x*(4*Theta_x + 3)*(4*Theta_x + 2)*(4*Theta_x + 1) # D2 = Theta_y^2 - y*(2*Theta_y - Theta_x + 1)*(2*Theta_y - Theta_x) # # We test moving the ideal to the intersection with the locus of Strong Coupling. D1 = PFOperator( "theta0^2*(theta0 - 2*theta1) - 4*z0*(4*theta0 + 3)*(4*theta0 + 2)*(4*theta0 + 1)", no_variables=2, ) D2 = PFOperator( "theta1^2 - z1*(2*theta1 - theta0 + 1)*(2*theta1 - theta0)", no_variables=2, ) moved = PFIdeal([D1, D2]).change_coordinates("z0", "z1 - 1/4") op1, op2 = moved.operators solutions = moved.find_power_series_solution(indicials=[0, QQ(1) / 2], order=14) assert len(solutions) == 1 period = solutions[0] assert period.apply_operator(op1).coefficients == {} assert period.apply_operator(op2).coefficients == {} # Normalisation convention: the free parameter at the leading (0, 0) coefficient is 1. assert period.coefficients[(0, 0)][(0, 0)] == 1 assert period.coefficients[(0, 0)][(1, 0)] == 0 recovered = period.find_annihilating_operators(z_degree=1, theta_degree=2) assert len(recovered) == 1 assert recovered[0].operator == PFOperator( "-2*z1*theta0^2 + 8*z1*theta0*theta1 - 8*z1*theta1^2 + 2*z1*theta0 - 4*z1*theta1 " "+ 2*theta0*theta1 - 2*theta1^2 + theta1", no_variables=2, ).operator def test_single_operator_recovers_itself_from_its_holomorphic_period(): # Ensuring methods work for operator 4.2.1: L = PFOperator( "theta0^4 - 4*z0*(2*theta0 + 1)^2*(7*theta0^2 + 7*theta0 + 2) " "- 128*z0^2*(2*theta0 + 1)^2*(2*theta0 + 3)^2", no_variables=1, ) ideal = PFIdeal([L]) solutions = ideal.find_power_series_solution(indicials=[0], order=15) assert len(solutions) == 1 period = solutions[0] assert period.apply_operator(L).coefficients == {} assert period.coefficients[(0,)][(0,)] == 1 assert period.coefficients[(0,)][(1,)] == 8 assert period.coefficients[(0,)][(2,)] == 360 # L lives entirely within z_degree <= 2, theta_degree <= 4, its stated level. recovered = period.find_annihilating_operators(z_degree=2, theta_degree=4) assert len(recovered) == 1 # recovered[0] should be a scalar multiple of L - compare via the coefficient of the # bare theta0^4 monomial, which is 1 in L. ratio = recovered[0].operator.monomial_coefficient(L.theta_gens[0] ** 4) / L.operator.monomial_coefficient( L.theta_gens[0] ** 4 ) assert ratio != 0 assert recovered[0].operator == ratio * L.operator def test_indicials_of_quintic_at_mum_conifold_and_orbifold_points(): # At the MUM point all four indicials are 0; at the conifold point they are 0, 1, 1, 2, the doubled 1 # carrying the single logarithmic solution; at the orbifold point z0 -> 1/z0 they are 1/5, ..., 4/5. ideal = _quintic_ideal() assert ideal.indicials() == [((0,), 4)] assert ideal.change_coordinates("1 - 3125*z0").indicials() == [((0,), 1), ((1,), 2), ((2,), 1)] assert ideal.change_coordinates("1/z0").indicials() == [((QQ(k) / 5,), 1) for k in range(1, 5)] def test_indicials_reject_hodge_numbers_inconsistent_with_the_operators(): # The quintic's operator has rank 4, so its indicials can't add up to the rank 5 of these Hodge numbers. ideal = PFIdeal(_quintic_ideal().operators, hodge_numbers=[1, 1, 1, 1, 1]) with pytest.raises(ValueError): ideal.indicials() def test_frobenius_basis_of_quintic_at_mum_point(): # The solutions start with 1, log(z0), log(z0)^2 and log(z0)^3, the first being the classic holomorphic # period. Each solution's top-log coefficient is a solution without logs, hence that period again. ideal = _quintic_ideal() basis = ideal.frobenius_basis(order=4) assert [_starting_term(period) for period in basis] == [((0,), {(j,): 1}) for j in range(4)] holomorphic = {(0,): 1, (1,): 120, (2,): 113400, (3,): 168168000, (4,): 305540235000} assert basis[0].coefficients == {(0,): holomorphic} assert all(period.coefficients[(j,)] == holomorphic for j, period in enumerate(basis)) assert all(period.apply_operator(op).coefficients == {} for period in basis for op in ideal.operators) def test_frobenius_basis_of_quintic_at_conifold_point(): # The indicials 0, 1, 1, 2 differ by integers, so all four solutions come out of one solve from exponent 0. # Picard-Lefschetz: exactly one of them is logarithmic, z0*log(z0) + ..., starting where the vanishing # period z0 + ... does. conifold = _quintic_ideal().change_coordinates("1 - 3125*z0") basis = conifold.frobenius_basis(order=4) assert [_starting_term(period) for period in basis] == [ ((0,), {(0,): 1}), ((1,), {(0,): 1}), ((1,), {(1,): 1}), ((2,), {(0,): 1}), ] assert [max(log_index[0] for log_index in period.coefficients) for period in basis] == [0, 0, 1, 0] assert all(period.apply_operator(op).coefficients == {} for period in basis for op in conifold.operators) def test_frobenius_basis_of_quintic_at_orbifold_point(): # Four exponent classes 1/5, ..., 4/5 mod 1, each with a single solution without logs. Order 6 reaches # the step at which Sage's echelon form of sparse rational matrices went wrong (see operators.sage). orbifold = _quintic_ideal().change_coordinates("1/z0") basis = orbifold.frobenius_basis(order=6) assert [period.indicials for period in basis] == [[QQ(k) / 5] for k in range(1, 5)] assert all(list(period.coefficients) == [(0,)] for period in basis) assert all(period.apply_operator(op).coefficients == {} for period in basis for op in orbifold.operators) def test_frobenius_basis_of_two_parameter_model_at_mum_point(): # All six indicials are (0, 0), and the solutions start with 1; log(z0), log(z1); log(z0)^2, # log(z0)*log(z1); and a triple-log term proportional to kappa_ijk*log(z_i)*log(z_j)*log(z_k) with the # intersection numbers kappa_000 = 8, kappa_001 = 4: the log structure [1, 2, 2, 1] of the Hodge numbers. ideal = _p22211_ideal() assert ideal.indicials() == [((0, 0), 6)] basis = ideal.frobenius_basis(order=3) assert [_starting_term(period) for period in basis] == [ ((0, 0), {(0, 0): 1}), ((0, 0), {(1, 0): 1}), ((0, 0), {(0, 1): 1}), ((0, 0), {(2, 0): 1}), ((0, 0), {(1, 1): 1}), ((0, 0), {(3, 0): 1, (2, 1): QQ(3) / 2}), ] assert all(period.apply_operator(op).coefficients == {} for period in basis for op in ideal.operators) def test_frobenius_basis_of_two_parameter_model_at_strong_coupling(): # At (z0, z1) = (0, 1/4) the generators' z-free parts theta0^2*theta1 and theta1*(2*theta1 - 2*theta0 - 1) # vanish on the whole line theta1 = 0. Completing the indicial ideal pins the indicials down to (0, 0) # with multiplicity 4 and (0, 1/2) with multiplicity 2. The latter class holds sqrt(z1)*(1 + ...), the # power series solution of test_ideal_finds_power_series_solution_and_recovers_first_operator, and a # logarithmic partner sqrt(z1)*(log(z0) + log(z1) + ...). moved = _p22211_ideal().change_coordinates("z0", "z1 - 1/4") assert moved.indicials() == [((0, 0), 4), ((0, QQ(1) / 2), 2)] basis = moved.frobenius_basis(order=4) assert [period.indicials for period in basis] == [[0, 0]] * 4 + [[0, QQ(1) / 2]] * 2 assert [_starting_term(period) for period in basis] == [ ((0, 0), {(0, 0): 1}), ((0, 0), {(1, 0): 1}), ((0, 0), {(2, 0): 1}), ((0, 0), {(3, 0): 1}), ((0, 0), {(0, 0): 1}), ((0, 0), {(1, 0): 1, (0, 1): 1}), ] power_series = moved.find_power_series_solution(indicials=[0, QQ(1) / 2], order=4) assert basis[4].coefficients == power_series[0].coefficients assert all(period.apply_operator(op).coefficients == {} for period in basis for op in moved.operators) def test_frobenius_basis_warns_when_log_degree_is_capped_too_low(caplog): # The triple-log solution at the MUM point needs log-degree 3, so with max_log_degree=2 only five of the # six solutions survive - which is reported rather than silently returned. ideal = _p22211_ideal() with caplog.at_level(logging.WARNING): basis = ideal.frobenius_basis(order=2, max_log_degree=2) assert len(basis) == 5 assert any("rank sum(hodge_numbers) is 6" in record.getMessage() for record in caplog.records) def test_frobenius_basis_of_two_parameter_fourfold(caplog): # At the MUM point the nine solutions have the log structure [1, 2, 3, 2, 1], and the top ones are given by # the intersection numbers: the quartic one by kappa_ijkl*log(z_i)*...*log(z_l), i.e. 36*log(z0)^4 + # 48*log(z0)^3*log(z1) + 24*log(z0)^2*log(z1)^2 up to scale, a cubic one by its derivative in log(z1). ideal = _p333111_ideal() assert ideal.indicials() == [((0, 0), 9)] basis = ideal.frobenius_basis(order=3) assert [_starting_term(period) for period in basis] == [ ((0, 0), {(0, 0): 1}), ((0, 0), {(1, 0): 1}), ((0, 0), {(0, 1): 1}), ((0, 0), {(2, 0): 1}), ((0, 0), {(1, 1): 1}), ((0, 0), {(0, 2): 1}), ((0, 0), {(3, 0): 1, (2, 1): 1}), ((0, 0), {(1, 2): 1}), ((0, 0), {(4, 0): 1, (3, 1): QQ(4) / 3, (2, 2): QQ(2) / 3}), ] assert all(period.apply_operator(op).coefficients == {} for period in basis for op in ideal.operators) # At (0, -1/27), where the strong coupling divisor 1 + 27*z1 = 0 crosses the MUM divisor, all indicials lie # in one exponent class and there is a log(z0)^4 solution. But some indicials are (0, 1): this is no MUM # point, and its log structure [2, 3, 2, 1, 1] must not be compared with the Hodge numbers. moved = ideal.change_coordinates("z0", "z1 + 1/27") with caplog.at_level(logging.WARNING): basis = moved.frobenius_basis(order=3) assert moved.indicials() == [((0, 0), 5), ((0, 1), 4)] assert len(basis) == 9 assert all(period.apply_operator(op).coefficients == {} for period in basis for op in moved.operators) assert not caplog.records def test_frobenius_bases_at_mum_points_agree_with_the_classical_frobenius_method(): # An independent construction of every period at the MUM points, logarithmic ones included: the classical # Frobenius method, from nothing but the Gamma-function coefficients c(k) of the fundamental period. The # starting terms are those of the canonical bases; for P_{22211}[8] the cubic one is # kappa_ijk*eps_i*eps_j*eps_k/8 with the intersection numbers kappa_000 = 8, kappa_001 = 4. eps_ring = PolynomialRing(QQ, 1, ["e0"]) e = eps_ring.gen(0) def quintic_ratio(k): # c(n + eps)/c(eps) for c(n) = (5n)!/(n!)^5 ratio = eps_ring(1) for i in range(1, 5 * k[0] + 1): ratio = _truncated(ratio * (5 * e + i), 3) for i in range(1, k[0] + 1): ratio = _truncated(ratio * _inverse_of_linear(i, e, 3) ** 5, 3) return ratio classical = _classical_frobenius_periods(quintic_ratio, [e**j for j in range(4)], order=8) basis = _quintic_ideal().frobenius_basis(order=8) assert [_nonzero(period.coefficients) for period in basis] == [_nonzero(c) for c in classical] eps_ring = PolynomialRing(QQ, 2, ["e0", "e1"]) e0, e1 = eps_ring.gens() def p22211_ratio(k): # c(n + eps0, m + eps1)/c(eps0, eps1) for c(n, m) = (4n)!/((n!)^3 (m!)^2 (n - 2m)!). For n < 2m the # last factor is Gamma(1 + delta)/Gamma(1 + delta + n - 2m) with delta = eps0 - 2*eps1, a polynomial # vanishing at eps = 0, which only the logarithmic periods see. n, m = k delta = e0 - 2 * e1 ratio = eps_ring(1) for i in range(1, 4 * n + 1): ratio = _truncated(ratio * (4 * e0 + i), 3) for i in range(1, n + 1): ratio = _truncated(ratio * _inverse_of_linear(i, e0, 3) ** 3, 3) for i in range(1, m + 1): ratio = _truncated(ratio * _inverse_of_linear(i, e1, 3) ** 2, 3) for i in range(1, n - 2 * m + 1): ratio = _truncated(ratio * _inverse_of_linear(i, delta, 3), 3) for i in range(2 * m - n): ratio = _truncated(ratio * (delta - i), 3) return ratio starting_terms = [eps_ring(1), e0, e1, e0**2, e0 * e1, e0**3 + QQ(3) / 2 * e0**2 * e1] classical = _classical_frobenius_periods(p22211_ratio, starting_terms, order=6) basis = _p22211_ideal().frobenius_basis(order=6) assert [_nonzero(period.coefficients) for period in basis] == [_nonzero(c) for c in classical] def test_quintic_bases_at_mum_and_conifold_point_continue_into_each_other(): # Both expansions converge on the lens between z0 = 0 and z0 = 1/3125, where the MUM periods must be # constant linear combinations T of the conifold periods. Through T the conifold monodromy # (log w -> log w + 2*pi*i) becomes a Picard-Lefschetz transvection in the MUM basis, and its product with # the MUM monodromy is the monodromy around z0 = oo, of order 5. CF = ComplexField(200) two_pi_i = 2 * CF.pi() * CF.gen() ideal = _quintic_ideal() mum = ideal.frobenius_basis(order=100) conifold = ideal.change_coordinates("1 - 3125*z0").frobenius_basis(order=100) def values(basis, points, shifted=False): # One row per point z0, one column per period, continued once around w = 0 if shifted. rows = [] for z0 in points: w = z0 if basis is mum else 1 - 3125 * z0 log_w = CF(w).log() + (two_pi_i if shifted else 0) rows.append([ sum( CF(coeff) * log_w ** log_index[0] * ((z_index[0] + period.indicials[0]) * log_w).exp() for log_index, z_dict in period.coefficients.items() for z_index, coeff in z_dict.items() if coeff != 0 ) for period in basis ]) return matrix(CF, rows) fit = [CF(x, y) / 3125 for x, y in [(0.4, -0.15), (0.5, 0.1), (0.6, -0.1), (0.45, 0.15)]] test = [CF(x, y) / 3125 for x, y in [(0.55, 0.05), (0.42, -0.05), (0.6, 0.12)]] T = values(conifold, fit).solve_right(values(mum, fit)).transpose() assert (values(mum, test) - values(conifold, test) * T.transpose()).norm() < 1e-8 * values(mum, test).norm() M_0 = values(mum, fit).solve_right(values(mum, fit, shifted=True)).transpose() M_c = T * values(conifold, fit).solve_right(values(conifold, fit, shifted=True)).transpose() * T.inverse() identity = identity_matrix(CF, 4) singular_values = (M_c - identity).change_ring(CDF).SVD()[1].diagonal() assert singular_values[1] < 1e-8 * singular_values[0] assert ((M_0 * M_c) ** 5 - identity).norm() < 1e-8