def character_variety(group): gens = group.generators() vars = [g + repr(i) for g in gens for i in range(4)] R = PolynomialRing(QQ, vars) mats = { g: matrix(R, [[R(g + '0'), R(g + '1')], [R(g + '2'), R(g + '3')]]) for g in gens } rels = [A.det() - 1 for A in mats.values()] for g in gens: mats[g.upper()] = matrix( R, [[R(g + '3'), -R(g + '1')], [-R(g + '2'), R(g + '0')]]) def to_mat(word): return prod(mats[w] for w in word) for word in group.relators(): w0, w1 = halfes(word) diff = to_mat(w0) - to_mat(w1) rels += diff.list() rels += [R('a2'), R('a1 - 1'), R('b1')] return R.ideal(rels)
def gluing_equation_ideal(manifold): n = manifold.num_tetrahedra() R = PolynomialRing(QQ, 'z', n) zs = R.gens() eqns = manifold.gluing_equations('rect') c_to_exp = {1: 0, -1: 1} hermite = matrix([a + b + [c_to_exp[c]] for a, b, c in eqns]).hermite_form() eqns = [(row[:n], row[n:2 * n], row[-1] % 2) for row in hermite if not (row[:-1] == 0 and row[-1] % 2 == 0)] return R.ideal([polynomial_eval_eqn(eqn, zs) for eqn in eqns])
def components_containing_irreducible_reps(G): R = PolynomialRing(QQ, ['x', 'y', 'z']) assert G.generators() == ['a', 'b'], len(G.relators()) == 1 r = G.relators()[0] I = R.ideal([tr(r[:j]) - tr(r[j:]) for j in range(len(r))] + [ tr('a' + r) - tr('a' + inverse_word(r)), tr('b' + r) - tr('b' + inverse_word(r)) ]).radical() contains_irreducible_rep = [ J for J in I.primary_decomposition() if not tr('abAB') - 2 in J ] return contains_irreducible_rep
def belongs_to_radical(f, I): """ Test if f in I.radical(). """ R = PolynomialRing(QQ, I.ring().ngens() + 1, I.ring().variable_names() + ('Zoo', )) Zoo = R.gens()[-1] J = R.ideal([R(g) for g in I.gens()] + [R(1) - Zoo * R(f)]) return [ R(1) ] == J.groebner_basis(algorithm='singular' if common.plumber else '')
def character_variety_ideal(gens, rels=None): """ sage: M = Manifold('m004') sage: I = character_variety_ideal(M.fundamental_group()) sage: I.dimension() 1 sage: len(I.radical().primary_decomposition()) 2 """ presentation = character_variety(gens, rels) from sage.all import PolynomialRing, QQ R = PolynomialRing(QQ, [repr(v) for v in presentation.gens]) return R.ideal([R(p) for p in presentation.rels])
def _to_polynomial(f, val1): prec = f.prec.value R = PolynomialRing(QQ if f.base_ring == ZZ else f.base_ring, names="q1, q2") q1, q2 = R.gens() I = R.ideal([q1 ** (prec + 1), q2 ** (prec + 1)]) S = R.quotient_ring(I) res = sum([sum([f.fc_dct.get((n, r, m), 0) * QQ(val1) ** r for r in range(-int(floor(2 * sqrt(n * m))), int(floor(2 * sqrt(n * m))) + 1)]) * q1 ** n * q2 ** m for n in range(prec + 1) for m in range(prec + 1)]) return S(res)
def gluing_equation_ideal_alt(manifold): n = manifold.num_tetrahedra() vars = [] for i in range(n): vars += ['z%d' % i, 'zp%d' % i, 'zpp%d' % i] R = PolynomialRing(QQ, vars) zs = R.gens() toric_eqns = matrix(manifold.gluing_equations('log')).hermite_form() eqns = [toric_to_poly(zs, eqn) for eqn in toric_eqns] for i in range(n): z, zp, zpp = zs[3 * i:3 * (i + 1)] eqns += [z * zp * zpp + 1, zp * (1 - z) - 1] return R.ideal(eqns)
def gluing_variety_ideal(manifold, vars_per_tet=1): manifold = manifold.copy() n = manifold.num_tetrahedra() var_names = ['z%d' % i for i in range(n)] ideal_gens = [] if vars_per_tet != 1: assert vars_per_tet == 2 var_names += ['w%d' % i for i in range(n)] R = PolynomialRing(ZZ, var_names) if vars_per_tet == 2: ideal_gens += [R('z%d + w%d - 1' % (i, i)) for i in range(n)] eqn_data = snappy.snap.shapes.enough_gluing_equations(manifold) ideal_gens += [ make_rect_eqn(R, A, B, c, vars_per_tet) for A, B, c in eqn_data ] return R.ideal(ideal_gens)
def _mixed_volume_gfan(gen): """ Use gfan to compute the mixed volume of a collection of polytopes. Just like Khovanskii, we use the normalised mixed volume which satisfies mixed_volume([P,...,P]) = volume(P). """ P = list(gen) n = len(P) if any(Q.ambient_dim() != n for Q in P): raise TypeError( 'Number of polytopes and ambient dimension do not match') if n == 0: return 0 elif n == 1: return P[0].volume() R = PolynomialRing(QQ, 'x', n) I = R.ideal([sum(monomial_exp(R, e) for e in Q.vertices()) for Q in P]) return ZZ.one() / Integer(factorial(n)) * I.groebner_fan().mixed_volume()
def gluing_phc_standalone(manifold): thingy = manifold_reps.PHCGluingSolutionsOfClosed(manifold) R = PolynomialRing(QQ, thingy.variables) I = R.ideal([R(eqn) for eqn in thingy.equations]) return phc_wrapper.find_solutions(I)
def verify_algebraically_GB(g, P0, alpha, trace_and_norm, verbose=True): # input: # * P0 (only necessary to shift the series) # * [trace_numerator, trace_denominator, norm_numerator, norm_denominator] # output: # a boolean if verbose: print "verify_algebraically()" L = P0.base_ring() assert alpha.base_ring() is L L_poly = PolynomialRing(L, "xL") xL = L_poly.gen() # shifting the series avoids makes our life easier trace_numerator, trace_denominator, norm_numerator, norm_denominator = [ L_poly(coeff)(L_poly.gen() + P0[0]) for coeff in trace_and_norm ] L_fpoly = L_poly.fraction_field() trace = L_fpoly(trace_numerator) / L_fpoly(trace_denominator) norm = L_fpoly(norm_numerator) / L_fpoly(norm_denominator) L_fpoly_Z2 = PolynomialRing(L_fpoly, "z") z = L_fpoly_Z2.gen() gP0 = g(L_poly.gen() + P0[0]) disc = (trace**2 - 4 * norm) IsqrtD = L_fpoly_Z2.ideal([z**2 - disc.numerator()]) R0 = L_fpoly_Z2.quotient_ring(IsqrtD) iz = z / R0(z**2).lift() x1 = R0((trace - z / sqrt(disc.denominator())) / 2).lift() x2 = R0((trace + z / sqrt(disc.denominator())) / 2).lift() assert x1 + x2 == trace, "x1 + x2" assert R0(x1 * x2) == norm, "x1 * x2" dx1 = R0( (trace.derivative(xL) - iz * sqrt(disc.denominator()) * disc.derivative(xL) / 2) / 2).lift() dx2 = R0( (trace.derivative(xL) + iz * sqrt(disc.denominator()) * disc.derivative(xL) / 2) / 2).lift() assert R0(dx1 + dx2) == R0(trace.derivative(xL)), "dx1 + dx2" gx1 = R0(g(x1)).lift() gx2 = R0(g(x2)).lift() igx1 = R0(gx2).lift() / R0(gx2 * gx1).lift() assert R0(gx1 * igx1) == 1, "gx1 * igx1" igx2 = R0(gx1).lift() / R0(gx1 * gx2).lift() assert R0(gx2 * igx2) == 1, "gx2 * igx2" square = gx1.numerator() if verbose: print "Simplifying sqrt( g(x1).numerator() )" a1, a2, d1, d2 = simplify_sqrt(square.constant_coefficient().numerator(), square.monomial_coefficient(z).numerator(), disc.numerator()) assert (d1.numerator() // gP0) in L or (d2.numerator() // gP0) in L, "d1 or d2" L_fpoly_Z = PolynomialRing(L_fpoly, 3, "z, y, w") z, y, w = L_fpoly_Z.gens() Isqrt = L_fpoly_Z.ideal([z - y * w, y**2 - d1, w**2 - d2]) R = L_fpoly_Z.quotient_ring(Isqrt) iz = z / (d1 * d2) assert R(z * iz) == 1 iw = w / R(w**2).lift() assert R(w * iw) == 1 iy = y / R(y**2).lift() assert R(iy * y) == 1 sgx1 = R(a1 * y + a2 * w).lift() / R(sqrt(gx1.denominator())).lift() sgx2 = R(a1 * y - a2 * w).lift() / R(sqrt(gx1.denominator())).lift() assert R(sgx1**2) == gx1, "sgx1**2" assert R(sgx2**2) == gx2, "sgx2**2" isgx1 = R(sgx2).lift() / R(sgx1 * sgx2).lift() isgx2 = R(sgx1).lift() / R(sgx1 * sgx2).lift() assert R(sgx1 * isgx1) == 1, "sgx1 * isgx1" assert R(sgx2 * isgx2) == 1, "sgx2 * isgx2" if verbose: print "adjusting to d1//g(x) or d1/g(x) in L" if R(w**2 / gP0).lift().degree() == 0: ct = iw * sqrt(R(w**2 / gP0).lift().constant_coefficient()) else: assert R(w**2 / gP0).lift().degree() == 0 ct = iy * sqrt(R(y**2 / gP0).lift().constant_coefficient()) # else: # assert R(y**2/gP0).lift() in L, "\n%s\n%s\n%s\n%s\n" % ( R(y**2/gP0).lift(), R(y**2/gP0).lift() in L, R(w**2/gP0).lift(), R(w**2/gP0).lift() in L, ) # ct = iy * sqrt(L(R(y**2/gP0).lift())) eq1 = Matrix([[ -2 * L_poly(alpha.row(0).list())(L_poly.gen() + P0[0]) * ct, R(dx1 * isgx1), R(dx2 * isgx2) ]]) eq2 = Matrix([[ -2 * L_poly(alpha.row(1).list())(L_poly.gen() + P0[0]) * ct, R(x1 * dx1 * isgx1), R(x2 * dx2 * isgx2) ]]) branches = Matrix( R, [[1, 1, 1], [1, 1, -1], [1, -1, 1], [1, -1, 1]]).transpose() meq1 = eq1 * branches meq2 = eq2 * branches algzero = False for j in range(4): if meq1[0, j] == 0 and meq2[0, j] == 0: algzero = True break if verbose: print "Done, verify_algebraically() = %s" % algzero return algzero
class MotionClassifier(SageObject): r""" This class implements the functionality for determining possible motions of a graph. """ def __init__(self, graph, four_cycles=[], separator='', edges_ordered=[]): if not (isinstance(graph, FlexRiGraph) or 'FlexRiGraph' in str(type(graph))): raise exceptions.TypeError('The graph must be of the type FlexRiGraph.') self._graph = graph if four_cycles == []: self._four_cycles = self._graph.four_cycles(only_with_NAC=True) else: self._four_cycles = four_cycles if not self._graph.are_NAC_colorings_named(): self._graph.set_NAC_colorings_names() # -----Polynomial Ring for leading coefficients----- ws = [] zs = [] lambdas = [] ws_latex = [] zs_latex = [] lambdas_latex = [] if edges_ordered==[]: edges_ordered = self._graph.edges(labels=False) else: if (Set([self._edge2str(e) for e in edges_ordered]) != Set([self._edge2str(e) for e in self._graph.edges(labels=False)])): raise ValueError('The provided ordered edges do not match the edges of the graph.') for e in edges_ordered: ws.append('w' + self._edge2str(e)) zs.append('z' + self._edge2str(e)) lambdas.append('lambda' + self._edge2str(e)) ws_latex.append('w_{' + self._edge2str(e).replace('_', separator) + '}') zs_latex.append('z_{' + self._edge2str(e).replace('_', separator) + '}') lambdas_latex.append('\\lambda_{' + self._edge2str(e).replace('_', separator) + '}') self._ringLC = PolynomialRing(QQ, names=lambdas+ws+zs) #, order='lex') self._ringLC._latex_names = lambdas_latex + ws_latex + zs_latex self._ringLC_gens = self._ringLC.gens_dict() self._ring_lambdas = PolynomialRing(QQ, names=lambdas + ['u']) self._ring_lambdas._latex_names = lambdas_latex + ['u'] self._ring_lambdas_gens = self._ring_lambdas.gens_dict() self.aux_var = self._ring_lambdas_gens['u'] xs = [] ys = [] xs_latex = [] ys_latex = [] for v in self._graph.vertices(): xs.append('x' + str(v)) ys.append('y' + str(v)) xs_latex.append('x_{' + str(v) + '}') ys_latex.append('y_{' + str(v) + '}') self._ring_coordinates = PolynomialRing(QQ, names=lambdas+xs+ys) self._ring_coordinates._latex_names = lambdas_latex + xs_latex + ys_latex self._ring_coordinates_gens = self._ring_coordinates.gens_dict() # ----Ramification----- # if len(self._graph.NAC_colorings()) > 1: self._ring_ramification = PolynomialRing(QQ, [col.name() for col in self._graph.NAC_colorings()], len(self._graph.NAC_colorings())) # else: # self._ring_ramification = PolynomialRing(QQ, self._graph.NAC_colorings()[0].name()) self._ring_ramification_gens = self._ring_ramification.gens_dict() self._restriction_NAC_types = self.NAC_coloring_restrictions() # -----Graph of 4-cycles----- self._four_cycle_graph = Graph([self._four_cycles,[]], format='vertices_and_edges') for c1, c2 in Subsets(self._four_cycle_graph.vertices(), 2): intersection = self.cycle_edges(c1, sets=True).intersection(self.cycle_edges(c2, sets=True)) if len(intersection)>=2 and len(intersection[0].intersection(intersection[1]))==1: common_vert = intersection[0].intersection(intersection[1])[0] self._four_cycle_graph.add_edge(c1, c2, common_vert) # -----Cycle with orthogonal diagonals due to NAC----- self._orthogonal_diagonals = { delta.name(): [cycle for cycle in self._four_cycle_graph if delta.cycle_has_orthogonal_diagonals(cycle)] for delta in self._graph.NAC_colorings()} @doc_index("Constraints on edge lengths") def four_cycles_ordered(self): r""" Heuristic order of 4-cycles. """ cliques = self._four_cycle_graph.cliques_maximal() cycles = max(cliques, key=lambda clique: sum([self._four_cycle_graph.degree(v) for v in clique])) missing_cliques = {tuple(clique):0 for clique in cliques} missing_cliques.pop(tuple(cycles)) while missing_cliques: next_clique = max(missing_cliques.keys(), key=lambda clique:sum([1 for c in clique for c2 in self._four_cycle_graph.neighbors(c) if c2 in cycles])) missing_cliques.pop(next_clique) missing_cycles = {c:0 for c in next_clique if not c in cycles} while missing_cycles: next_cycle = max(missing_cycles.keys(), key=lambda c:sum([1 for c2 in self._four_cycle_graph.neighbors(c) if c2 in cycles])) cycles.append(next_cycle) missing_cycles.pop(next_cycle) missing_cycles = {c:0 for c in self._four_cycle_graph.vertices() if not c in cycles} while missing_cycles: next_cycle = max(missing_cycles.keys(), key=lambda c:sum([1 for c2 in self._four_cycle_graph.neighbors(c) if c2 in cycles])) cycles.append(next_cycle) missing_cycles.pop(next_cycle) return cycles def _repr_(self): return 'Motion Classifier of ' + str(self._graph) @staticmethod def _edge2str(e): if e[0]<e[1]: return str(e[0]) + '_' + str(e[1]) else: return str(e[1]) + '_' + str(e[0]) @staticmethod @doc_index("Other") def cycle_edges(cycle, sets=False): r""" Return edges of a 4-cycle. """ if sets: return Set([Set(list(e)) for e in zip(cycle, list(cycle[1:])+[cycle[0]])]) else: return [list(e) for e in zip(cycle, list(cycle[1:])+[cycle[0]])] @staticmethod @doc_index("Other") def four_cycle_normal_form(cycle, motion_type): r""" Return a 4-cycle with a motion type in the normal form. """ i = cycle.index(min(cycle)) oe = ['o', 'e'] if i % 2 == 1 and motion_type in oe: motion_type = oe[1 - oe.index(motion_type)] tmp_c = cycle[i:]+cycle[:i] if tmp_c[1]<tmp_c[3]: return tmp_c, motion_type else: return (tmp_c[0], tmp_c[3], tmp_c[2], tmp_c[1]), motion_type @staticmethod @doc_index("Other") def normalized_motion_types(motion_types): r""" Return motion types in the normal form. """ res = {} for c, t in motion_types.items(): norm_c, norm_t = MotionClassifier.four_cycle_normal_form(c, t) res[norm_c] = norm_t return res def _w(self, e): if e[0] < e[1]: return self._ringLC_gens['w'+self._edge2str(e)] else: return -self._ringLC_gens['w'+self._edge2str(e)] def _z(self, e): if e[0] < e[1]: return self._ringLC_gens['z'+self._edge2str(e)] else: return -self._ringLC_gens['z'+self._edge2str(e)] def _lam(self, e): return self._ringLC_gens['lambda'+self._edge2str(e)] @doc_index("Constraints on edge lengths") def lam(self, u,v): r""" Return the variable for edge length in the ring of edge lengths. """ return self._ring_lambdas_gens['lambda'+self._edge2str([u,v])] @doc_index("Motion types consistent with 4-cycles") def mu(self, delta): r""" Return the variable for a given NAC-coloring. """ if type(delta)==str: return self._ring_ramification_gens[delta] else: return self._ring_ramification_gens[delta.name()] @doc_index("System of equations for coordinates") def x(self, v): r""" Return the variable for x coordinate of a vertex. """ return self._ring_coordinates_gens['x'+str(v)] @doc_index("System of equations for coordinates") def y(self, v): r""" Return the variable for y coordinate of a vertex. """ return self._ring_coordinates_gens['y'+str(v)] @doc_index("System of equations for coordinates") def l(self, u,v): r""" Return the variable for edge length in the ring with coordinates. """ return self._ring_coordinates_gens['lambda'+self._edge2str([u,v])] @doc_index("Constraints on edge lengths") def equations_from_leading_coefs(self, delta, extra_eqs=[], check=True): r""" Return equations for edge lengths from leading coefficients system. EXAMPLES:: sage: from flexrilog import GraphGenerator, MotionClassifier sage: K33 = GraphGenerator.K33Graph() sage: M = MotionClassifier(K33) sage: M.equations_from_leading_coefs('epsilon56') [lambda1_2^2 - lambda1_4^2 - lambda2_3^2 + lambda3_4^2] :: sage: M.equations_from_leading_coefs('omega1') Traceback (most recent call last): ... ValueError: The NAC-coloring must be a singleton. :: sage: M.equations_from_leading_coefs('omega1', check=False) [lambda2_5^2*lambda3_4^2 - lambda2_5^2*lambda3_6^2 - lambda2_3^2*lambda4_5^2 + lambda3_6^2*lambda4_5^2 + lambda2_3^2*lambda5_6^2 - lambda3_4^2*lambda5_6^2] """ if type(delta) == str: delta = self._graph.name2NAC_coloring(delta) if check: if not delta.is_singleton(): raise exceptions.ValueError('The NAC-coloring must be a singleton.') eqs_lengths=[] for e in self._graph.edges(): eqs_lengths.append(self._z(e)*self._w(e) - self._lam(e)**_sage_const_2) eqs_w=[] eqs_z=[] for T in self._graph.spanning_trees(): for e in self._graph.edges(): eqw = 0 eqw_all = 0 eqz = 0 eqz_all = 0 path = T.shortest_path(e[0],e[1]) for u,v in zip(path, path[1:]+[path[0]]): if delta.is_red(u,v): eqz+=self._z([u,v]) else: eqw+=self._w([u,v]) eqw_all+=self._w([u,v]) eqz_all+=self._z([u,v]) if eqw: eqs_w.append(eqw) else: eqs_w.append(eqw_all) if eqz: eqs_z.append(eqz) else: eqs_z.append(eqz_all) equations = (ideal(eqs_w).groebner_basis() + ideal(eqs_z).groebner_basis() + eqs_lengths + [self._ringLC(eq) for eq in extra_eqs]) return [self._ring_lambdas(eq) for eq in ideal(equations).elimination_ideal(flatten( [[self._w(e), self._z(e)] for e in self._graph.edges()])).basis ] @staticmethod def _pair_ordered(u,v): if u<v: return (u, v) else: return (v, u) @staticmethod def _edge_ordered(u,v): return MotionClassifier._pair_ordered(u, v) # @staticmethod def _set_two_edge_same_lengths(self, H, u, v, w, y, k): if H[self._edge_ordered(u,v)]==None and H[self._edge_ordered(w,y)]==None: H[self._edge_ordered(u,v)] = k H[self._edge_ordered(w,y)] = k return 1 elif H[self._edge_ordered(u,v)]==None: H[self._edge_ordered(u,v)] = H[self._edge_ordered(w,y)] return 0 elif H[self._edge_ordered(w,y)]==None: H[self._edge_ordered(w,y)] = H[self._edge_ordered(u,v)] return 0 elif H[self._edge_ordered(u,v)]!=H[self._edge_ordered(w,y)]: col= H[self._edge_ordered(u,v)] for u,v in H.keys(): if H[(u,v)]==col: H[(u,v)] = H[self._edge_ordered(w,y)] return 0 return 0 def _set_same_lengths(self, H, types): for u,v in H.keys(): H[(u,v)] = None k=1 for c in types: motion = types[c] if motion=='a' or motion=='p': k += self._set_two_edge_same_lengths(H, c[0], c[1], c[2], c[3], k) k += self._set_two_edge_same_lengths(H, c[1], c[2], c[0], c[3], k) elif motion=='o': k += self._set_two_edge_same_lengths(H, c[0], c[1], c[1], c[2], k) k += self._set_two_edge_same_lengths(H, c[2], c[3], c[0], c[3], k) elif motion=='e': k += self._set_two_edge_same_lengths(H, c[1], c[2], c[2], c[3], k) k += self._set_two_edge_same_lengths(H, c[0], c[1], c[0], c[3], k) @doc_index("Constraints on edge lengths") def motion_types2same_edge_lenghts(self, motion_types): r""" Return the dictionary of same edge lengths enforced by given motion types. """ H = {self._edge_ordered(u,v):None for u,v in self._graph.edges(labels=False)} self._set_same_lengths(H, motion_types) return H @doc_index("Motion types consistent with 4-cycles") def NAC_coloring_restrictions(self): r""" Return types of restrictions of NAC-colorings to 4-cycles. EXAMPLE:: sage: from flexrilog import MotionClassifier, GraphGenerator sage: MC = MotionClassifier(GraphGenerator.K33Graph()) sage: MC.NAC_coloring_restrictions() {(1, 2, 3, 4): {'L': ['omega3', 'omega1', 'epsilon36', 'epsilon16'], 'O': ['epsilon34', 'epsilon14', 'epsilon23', 'epsilon12'], 'R': ['omega4', 'epsilon45', 'omega2', 'epsilon25']}, ... (3, 4, 5, 6): {'L': ['omega5', 'omega3', 'epsilon25', 'epsilon23'], 'O': ['epsilon56', 'epsilon36', 'epsilon45', 'epsilon34'], 'R': ['omega6', 'epsilon16', 'omega4', 'epsilon14']}} """ res = {cycle:{'O':[], 'L':[], 'R':[]} for cycle in self._four_cycles} for delta in self._graph.NAC_colorings(): for cycle in self._four_cycles: colors = [delta.color(e) for e in self.cycle_edges(cycle)] if colors[0]==colors[1]: if colors[0]!=colors[2]: res[cycle]['R'].append(delta.name()) elif colors[1]==colors[2]: res[cycle]['L'].append(delta.name()) else: res[cycle]['O'].append(delta.name()) return res @doc_index("Motion types consistent with 4-cycles") def ramification_formula(self, cycle, motion_type): r""" Return ramification formula for a given 4-cycle and motion type. EXAMPLES:: sage: from flexrilog import MotionClassifier, GraphGenerator sage: MC = MotionClassifier(GraphGenerator.K33Graph()) sage: MC.ramification_formula((1,2,3,4), 'a') [epsilon34, epsilon14, epsilon23, epsilon12, omega3 + omega1 + epsilon36 + epsilon16 - omega4 - epsilon45 - omega2 - epsilon25] """ eqs_present = [] eqs_zeros = [] NAC_types = self.motion_types2NAC_types(motion_type) for t in ['L','O','R']: if t in NAC_types: eqs_present.append(sum([self.mu(delta) for delta in self._restriction_NAC_types[cycle][t]])) else: eqs_zeros += [self.mu(delta) for delta in self._restriction_NAC_types[cycle][t]] if 0 in eqs_present: return [self.mu(delta) for delta in self._graph.NAC_colorings()] if len(eqs_present)==2: return eqs_zeros + [eqs_present[0] - eqs_present[1]] elif len(eqs_present)==3: return eqs_zeros + [eqs_present[0] - eqs_present[1], eqs_present[1] - eqs_present[2]] else: return eqs_zeros @staticmethod @doc_index("Other") def motion_types2NAC_types(m): r""" Return NAC-coloring types for a given motion type. """ if m=='g': return ['L','R','O'] if m=='a': return ['L','R'] if m=='p': return ['O'] if m=='e': return ['R','O'] if m=='o': return ['L','O'] @staticmethod @doc_index("Other") def NAC_types2motion_type(t): r""" Return the motion type for given types of NAC-colorings. """ if Set(t)==Set(['L','R','O']): return 'g' if Set(t)==Set(['L','R']): return 'a' if Set(t)==Set(['O']): return 'p' if Set(t)==Set(['R','O']): return 'e' if Set(t)==Set(['L','O']): return 'o' @doc_index("Other") def active_NACs2motion_types(self, active): r""" Return the motion types of 4-cycles for a given set of active NAC-colorings. """ motion_types = {cycle:[] for cycle in self._four_cycles} for delta in active: if type(delta)!=str: delta = delta.name() for cycle in motion_types: motion_types[cycle] += [t for t, colorings in self._restriction_NAC_types[cycle].items() if delta in colorings] for cycle in motion_types: motion_types[cycle] = self.NAC_types2motion_type(motion_types[cycle]) return motion_types @staticmethod def _same_edge_lengths(K, edges_to_check): if edges_to_check: length = K[MotionClassifier._edge_ordered(edges_to_check[0][0], edges_to_check[0][1])] if length==None: return False for u,v in edges_to_check: if length!=K[MotionClassifier._edge_ordered(u,v)]: return False return True else: return True @doc_index("Motion types consistent with 4-cycles") def consequences_of_nonnegative_solution_assumption(self, eqs): r""" Return equations implied by the assumption of the existence of nonnegative solutions. """ n_zeros_prev = -1 zeros = [] gb = eqs while n_zeros_prev!=len(zeros): n_zeros_prev = len(zeros) gb = self._ring_ramification.ideal(gb + zeros).groebner_basis() # gb = self._ring_ramification.ideal(gb + zeros).groebner_basis() zeros = [] for eq in gb: coefs = eq.coefficients() if sum([sgn(a)*sgn(b) for a,b in zip(coefs[:-1],coefs[1:])])==len(coefs)-1: zeros += eq.variables() return [zeros, gb] # @staticmethod # def consequences_of_nonnegative_solution_assumption(eqs): # n_zeros_prev = -1 # zeros = {} # gb = ideal(eqs).groebner_basis() # while n_zeros_prev!=len(zeros): # n_zeros_prev = len(zeros) # gb = [eq.substitute(zeros) for eq in gb if eq.substitute(zeros)!=0] # for eq in gb: # coefs = eq.coefficients() # if sum([sgn(a)*sgn(b) for a,b in zip(coefs[:-1],coefs[1:])])==len(coefs)-1: # for zero_var in eq.variables(): # zeros[zero_var] = 0 # return [zeros.keys(), gb] @doc_index("Motion types consistent with 4-cycles") def consistent_motion_types(self):#, cycles=[]): r""" Return the list of motion types consistent with 4-cycles. """ # if cycles==[]: cycles = self.four_cycles_ordered() k23s = [Graph(self._graph).subgraph(k23_ver).edges(labels=False) for k23_ver in self._graph.induced_K23s()] aa_pp = [('a', 'a'), ('p', 'p')] ao = [('a','o'), ('o','a')] ae = [('a','e'), ('e','a')] oe = [('o', 'e'), ('e', 'o')] oo_ee = [('e','e'), ('o','o')] H = {self._edge_ordered(u,v):None for u,v in self._graph.edges(labels=False)} types_prev=[[{}, []]] self._num_tested_combinations = 0 for i, new_cycle in enumerate(cycles): types_ext = [] new_cycle_neighbors = [[c2, new_cycle.index(self._four_cycle_graph.edge_label(new_cycle, c2)), c2.index(self._four_cycle_graph.edge_label(new_cycle, c2)), ] for c2 in self._four_cycle_graph.neighbors(new_cycle) if c2 in cycles[:i]] for types_original, ramification_eqs_prev in types_prev: for type_new_cycle in ['g','a','p','o','e']: self._num_tested_combinations +=1 types = deepcopy(types_original) types[tuple(new_cycle)] = type_new_cycle # H = deepcopy(orig_graph) inconsistent = False for c2, new_index, c2_index in new_cycle_neighbors: type_pair = (types[new_cycle], types[c2]) if (type_pair in aa_pp or (type_pair in oe and new_index%2 == c2_index%2) or (type_pair in oo_ee and new_index%2 != c2_index%2)): inconsistent = True break if type_pair in ao: ind_o = type_pair.index('o') if [new_index, c2_index][ind_o] % 2 == 1: # odd deltoid (1,2,3,4) is consistent with 'a' if the common vertex is odd, # but Python lists are indexed from 0 inconsistent = True break if type_pair in ae: ind_e = type_pair.index('e') if [new_index, c2_index][ind_e] % 2 == 0: inconsistent = True break if inconsistent: continue self._set_same_lengths(H, types) for c in types: if types[c]=='g': labels = [H[self._edge_ordered(c[i-1],c[i])] for i in range(0,4)] if (not None in labels and ((len(Set(labels))==2 and labels.count(labels[0])==2) or len(Set(labels))==1)): inconsistent = True break if inconsistent: continue for K23_edges in k23s: if MotionClassifier._same_edge_lengths(H, K23_edges): inconsistent = True break if inconsistent: continue ramification_eqs = ramification_eqs_prev + self.ramification_formula(new_cycle, type_new_cycle) zero_variables, ramification_eqs = self.consequences_of_nonnegative_solution_assumption(ramification_eqs) for cycle in types: if inconsistent: break for t in self.motion_types2NAC_types(types[cycle]): has_necessary_NAC_type = False for delta in self._restriction_NAC_types[cycle][t]: if not self.mu(delta) in zero_variables: has_necessary_NAC_type = True break if not has_necessary_NAC_type: inconsistent = True break if inconsistent: continue types_ext.append([types, ramification_eqs]) types_prev=types_ext return [t for t, _ in types_prev] @doc_index("Other") def active_NAC_coloring_names(self, motion_types): r""" Return the names of active NAC-colorings for given motion types. """ return [delta.name() for delta in self.motion_types2active_NACs(motion_types)] @doc_index("Motion types consistent with 4-cycles") def motion_types2active_NACs(self, motion_types): r""" Return the active NAC-colorings for given motion types, if uniquely determined. """ zeros, eqs = self.consequences_of_nonnegative_solution_assumption( flatten([self.ramification_formula(c, motion_types[c]) for c in motion_types])) if self._ring_ramification.ideal(eqs).dimension()==1: return [delta for delta in self._graph.NAC_colorings() if not self.mu(delta) in zeros] else: raise NotImplementedError('There might be more solutions (dim '+str( self._ring_ramification.ideal(eqs).dimension()) + ')') @doc_index("General methods") def motion_types_equivalent_classes(self, motion_types_list): r""" Split a list of motion types into isomorphism classes. """ aut_group = self._graph.automorphism_group() classes = [ [( motion_types_list[0], self.normalized_motion_types( motion_types_list[0]), Counter([('d' if t in ['e','o'] else t) for c, t in motion_types_list[0].items()]))] ] for next_motion in motion_types_list[1:]: added = False next_sign = Counter([('d' if t in ['e','o'] else t) for c, t in next_motion.items()]) for cls in classes: repr_motion_types = cls[0][1] if cls[0][2]!=next_sign: continue for sigma in aut_group: next_motion_image = self.normalized_motion_types({tuple(sigma(v) for v in c): t for c,t in next_motion.items()}) for c in repr_motion_types: if repr_motion_types[c]!=next_motion_image[c]: break else: cls.append([next_motion]) added = True break # if not False in [repr_motion_types[c]==next_motion_image[c] for c in repr_motion_types]: # cls.append(next_motion) # added = True # break if added: break else: classes.append([(next_motion, self.normalized_motion_types(next_motion), next_sign)]) return [[t[0] for t in cls] for cls in classes] @doc_index("General methods") def check_orthogonal_diagonals(self, motion_types, active_NACs, extra_cycles_orthog_diag=[]): r""" Check the necessary conditions for orthogonal diagonals. TODO: return orthogonality_graph """ perp_by_NAC = [cycle for delta in active_NACs for cycle in self._orthogonal_diagonals[delta]] deltoids = [cycle for cycle, t in motion_types.items() if t in ['e','o']] orthogonalLines = [] for perpCycle in perp_by_NAC + deltoids + extra_cycles_orthog_diag: orthogonalLines.append(Set([Set([perpCycle[0],perpCycle[2]]), Set([perpCycle[1],perpCycle[3]])])) orthogonalityGraph = Graph(orthogonalLines, format='list_of_edges', multiedges=False) n_edges = -1 while n_edges != orthogonalityGraph.num_edges(): n_edges = orthogonalityGraph.num_edges() for perp_subgraph in orthogonalityGraph.connected_components_subgraphs(): isBipartite, partition = perp_subgraph.is_bipartite(certificate=True) if isBipartite: graph_0 = Graph([v.list() for v in partition if partition[v]==0]) graph_1 = Graph([v.list() for v in partition if partition[v]==1]) for comp_0 in graph_0.connected_components(): for comp_1 in graph_1.connected_components(): for e0 in Subsets(comp_0,2): for e1 in Subsets(comp_1,2): orthogonalityGraph.add_edge([Set(e0), Set(e1)]) else: raise exceptions.RuntimeError('A component of the orthogonality graph is not bipartite!') self._orthogonality_graph = orthogonalityGraph check_again = False H = {self._edge_ordered(u,v):None for u,v in self._graph.edges(labels=False)} self._set_same_lengths(H, motion_types) for c in motion_types: if not orthogonalityGraph.has_edge(Set([c[0],c[2]]),Set([c[1],c[3]])): continue if motion_types[c]=='a': # inconsistent since antiparallel motion cannot have orthogonal diagonals return False elif motion_types[c]=='p': # this cycle must be rhombus self._set_two_edge_same_lengths(H, c[0], c[1], c[2], c[3], 0) self._set_two_edge_same_lengths(H, c[0], c[1], c[1], c[2], 0) self._set_two_edge_same_lengths(H, c[0], c[1], c[0], c[3], 0) check_again = True for c in motion_types: if motion_types[c]=='g': labels = [H[self._edge_ordered(c[i-1],c[i])] for i in range(0,4)] if (not None in labels and ((len(Set(labels))==2 and labels.count(labels[0])==2) or len(Set(labels))==1)): return False if (orthogonalityGraph.has_edge(Set([c[0],c[2]]),Set([c[1],c[3]])) and True in [(H[self._edge_ordered(c[i-1], c[i])]==H[self._edge_ordered(c[i-2], c[i-1])] and H[self._edge_ordered(c[i-1],c[i])]!= None) for i in range(0,4)]): return False if check_again: for K23_edges in [Graph(self._graph).subgraph(k23_ver).edges(labels=False) for k23_ver in self._graph.induced_K23s()]: if MotionClassifier._same_edge_lengths(H, K23_edges): return False return True @doc_index("General methods") def possible_motion_types_and_active_NACs(self, comments = {}, show_table=True, one_representative=True, tab_rows=False, keep_orth_failed=False, equations=False): r""" Wraps the function for consistent motion types, conditions on orthogonality of diagonals and splitting into equivalence classes. """ types = self.consistent_motion_types() classes = self.motion_types_equivalent_classes(types) valid_classes = [] motions = [ 'g','a','p','d'] if one_representative: header = [['index', '#', 'motion types'] + motions + ['active NACs', 'comment']] else: header = [['index', '#', 'elem.', 'motion types'] + motions + ['active NACs', 'comment']] if equations: header[0].append('equations') rows = [] for i, cls in enumerate(classes): rows_cls = [] to_be_appended = True for j, t in enumerate(cls): row = [i, len(cls)] if not one_representative: row.append(j) row.append(' '.join([t[c] for c in self.four_cycles_ordered()])) row += [Counter([('d' if s in ['e','o'] else s) for c, s in t.items()])[m] for m in motions] try: active = self.active_NAC_coloring_names(t) row.append([self.mu(name) for name in sorted(active)]) if self.check_orthogonal_diagonals(t, active): row.append(comments.get(i,'')) else: to_be_appended = False if not keep_orth_failed: continue else: row.append('orthogonality check failed' + str(comments.get(i,''))) except NotImplementedError as e: zeros, eqs = self.consequences_of_nonnegative_solution_assumption( flatten([self.ramification_formula(c, t[c]) for c in t])) row.append([eq for eq in eqs if not eq in zeros]) row.append(str(comments.get(i,'')) + str(e)) if equations: zeros, eqs = self.consequences_of_nonnegative_solution_assumption( flatten([self.ramification_formula(c, t[c]) for c in t])) row.append([eq for eq in eqs if not eq in zeros]) rows_cls.append(row) if one_representative: break if to_be_appended or keep_orth_failed: valid_classes.append(cls) if one_representative: rows += rows_cls else: rows.append(rows_cls) if show_table: if one_representative: T = table(header + rows) else: T = table(header + [row for rows_cls in rows for row in rows_cls]) T.options()['header_row'] = True display(T) if tab_rows: return valid_classes, rows return valid_classes @doc_index("Constraints on edge lengths") def motion_types2equations(self, motion_types, active_NACs=None, groebner_basis=True, extra_eqs=[]): r""" Return equations enforced by edge lengths and singleton active NAC-colorings. """ if active_NACs==None: active_NACs = self.motion_types2active_NACs(motion_types) eqs_same_lengths = self.motion_types2same_lengths_equations(motion_types) eqs = flatten([self.equations_from_leading_coefs(delta, check=False, extra_eqs=eqs_same_lengths + extra_eqs) for delta in active_NACs if delta.is_singleton(active_NACs) ]) if groebner_basis: return ideal(eqs).groebner_basis() else: return eqs @doc_index("General methods") def degenerate_triangle_equation(self, u, v, w): r""" Return the equation for a degenerate triangle. """ return self.lam(u,v) - self.lam(u,w) - self.lam(w,v) @doc_index("Constraints on edge lengths") def motion_types2same_lengths_equations(self, motion_types): r""" Return the equations for edge lengths enforced by motion types. """ eqs = [] for c, motion in motion_types.items(): if motion=='a' or motion=='p': eqs.append(self.lam(c[0], c[1]) - self.lam(c[2], c[3])) eqs.append(self.lam(c[1], c[2]) - self.lam(c[0], c[3])) elif motion=='o': eqs.append(self.lam(c[0], c[1]) - self.lam(c[1], c[2])) eqs.append(self.lam(c[2], c[3]) - self.lam(c[0], c[3])) elif motion=='e': eqs.append(self.lam(c[1], c[2]) - self.lam(c[2], c[3])) eqs.append(self.lam(c[0], c[1]) - self.lam(c[0], c[3])) return [eq for eq in ideal(eqs).groebner_basis()] if eqs else [] @doc_index("Constraints on edge lengths") def graph_with_same_edge_lengths(self, motion_types, plot=True): r""" Return a graph with edge labels corresponding to same edge lengths. INPUT: - `plot` -- if `True` (default), then plot of the graph is returned. OUTPUT: The edge labels of the output graph are same for if the edge lengths are same due to `motion_types`. """ H = {self._edge_ordered(u,v):None for u,v in self._graph.edges(labels=False)} self._set_same_lengths(H, motion_types) G_labeled = Graph([[u,v,H[(u,v)]] for u,v in H]) G_labeled._pos = self._graph._pos if plot: return G_labeled.plot(edge_labels=True, color_by_label=True) else: return G_labeled @doc_index("Constraints on edge lengths") def singletons_table(self, active_NACs=None): r""" Return table whether (active) NAC-colorings are singletons. """ rows = [['NAC-coloring', 'is singleton']] if active_NACs==None: active_NACs = self._graph.NAC_colorings() only_active = False else: only_active = True rows[0].append('is singleton w.r.t. active') for delta in active_NACs: rows.append([delta.name(), delta.is_singleton()]) if only_active: rows[-1].append(delta.is_singleton(active_NACs)) T = table(rows) T.options()['header_row'] = True return T @doc_index("System of equations for coordinates") def edge_equations_ideal(self, fixed_edge, eqs_lamdas=[], extra_eqs=[], show_input=False): r""" Return the ideal of equations for coordinates of vertices and given edge constraints. """ equations = [] for u,v in self._graph.edges(labels=False): equations.append((self.x(u)-self.x(v))**_sage_const_2 + (self.y(u)-self.y(v))**_sage_const_2 - self.l(u,v)**_sage_const_2) equations += [ self.x(fixed_edge[0]), self.y(fixed_edge[0]), self.y(fixed_edge[1]), self.x(fixed_edge[1]) - self.l(fixed_edge[0], fixed_edge[1]), ] + [ self._ring_coordinates(eq) for eq in list(eqs_lamdas) + list(extra_eqs) ] if show_input: for eq in equations: show(eq) return ideal(equations) @doc_index("General methods") def edge_lengths_dimension(self, eqs_lambdas): r""" Return the dimension of the variaty of edge lengths. """ return ideal(eqs_lambdas + [self.aux_var]).dimension() @doc_index("Other") def edge_lengts_dict2eqs(self, edge_lengths): r""" Return equations with asigned edge lengths. """ return [self.lam(e[0],e[1]) - QQ(edge_lengths[e]) for e in edge_lengths ] @doc_index("General methods") def edge_lengths_satisfy_eqs(self, eqs, edge_lengths, print_values=False): r""" Check if a given dictionary of edge lengths satisfy given equations. """ I = ideal(self.edge_lengts_dict2eqs(edge_lengths)) if print_values: print([(eq.reduce(I)) for eq in eqs]) return sum([(eq.reduce(I))**2 for eq in eqs])==0 @staticmethod @doc_index("Other") def show_factored_eqs(eqs, only_print=False, numbers=False, variables=False, print_latex=False, print_eqs=True): r""" Show given equations factored. """ for i, eq in enumerate(eqs): factors = factor(eq) if numbers: print(i) if variables: print(latex(eq.variables())) if print_latex: print(latex(factors) + '=0\,, \\\\') if print_eqs: if only_print: print(factors) else: show(factors) @staticmethod @doc_index("General methods") def is_subcase(eqs_a, eqs_b): r""" Return if `eqs_a` is a subcase of `eqs_b`, i.e., the ideal of `eqs_a` contains the ideal of `eqs_b`. """ I_a = ideal(eqs_a) for eq in eqs_b: if not eq in I_a: return False return True @doc_index("Other") def motion_types2tikz(self, motion_types, color_names=[] , vertex_style='lnodesmall', none_gray=False, ): r""" Return TikZ code for the graph with edges colored according to the lengths enforced by motion types. """ H = {self._edge_ordered(u,v):None for u,v in self._graph.edges(labels=False)} self._set_same_lengths(H, motion_types) edge_partition = [[e for e in H if H[e]==el] for el in Set(H.values()) if el!=None] if none_gray: edge_partition.append([e for e in H if H[e]==None]) else: edge_partition += [[e] for e in H if H[e]==None] if color_names==[]: color_names = ['edge, col{}'.format(i) for i in range(1,len(edge_partition)+1)] if none_gray: color_names[-1] = 'edge' self._graph.print_tikz(colored_edges= edge_partition, color_names=color_names[:len(edge_partition)], vertex_style=vertex_style)
b = matrix(R, 2, 2, R.gens()[4:8]) c = matrix(R, 2, 2, R.gens()[8:12]) A = inverse_sl2(a) B = inverse_sl2(b) C = inverse_sl2(c) def eval_word(word): mats = {'a': a, 'b': b, 'c': c, 'A': A, 'B': B, 'C': C} return prod([mats[x] for x in word]) I = R.ideal([a.det() - 1, b.det() - 1, c.det() - 1] + sum([(eval_word(R) - 1).list() for R in G.relators()], []) + [eval_word(w)[1, 0] for w in periph] + [eval_word(w)[0, 0] - 1 for w in periph] + [eval_word(w)[1, 1] - 1 for w in periph] + [eval_word(periph[0])[0, 1] - 1, eval_word('a')[0, 1]]) # Magma confirms that I has dimension 0 via: # # sage: magma(I).Dimension() # 0 # # So there is a parbolic representation to SL(2, C) where every # parabolic has trace +2, and so ptolemy really is missing a rep. I # didn't check that the rep lands in SL(2, R). # Doesn't work because of PHC error "raised STORAGE_ERROR : stack
def extended_ptolemy_equations(manifold, gen_obs_class=None, nonzero_cond=True, return_full_var_dict=False, notation='short'): """ We assign ptolemy coordinates ['a', 'b', 'c', 'd', 'e', 'f'] to the *directed* edges:: [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)] where recall that the basic orientation convention of t3m and SnapPy is that a positively oriented simplex is as below. 1 /|\ d/ | \e / | \ / | \ 2----|----3 with back edge from 2 to 3 labelled f. \ | / b\ |a /c \ | / \|/ 0 sage: M = Manifold('m016') sage: I = extended_ptolemy_equations(M) sage: I.dimension() 1 """ if gen_obs_class is None: gen_obs_class = manifold.ptolemy_generalized_obstruction_classes(2)[0] m_star, l_star = peripheral.peripheral_cohomology_basis(manifold) n = manifold.num_tetrahedra() if notation == 'short': var_names = ['a', 'b', 'c', 'd', 'e', 'f'] first_var_name = 'a0' else: var_names = [ 'c_1100_', 'c_1010_', 'c_1001_', 'c_0110_', 'c_0101_', 'c_0011_' ] first_var_name = 'c_1100_0' tet_vars = [x + repr(d) for d in range(n) for x in var_names] def var(tet, edge): return tet_vars[6 * tet + directed_edges.index(edge)] all_arrows = arrows_around_edges(manifold) independent_vars = [var(a[0][0], a[0][2]) for a in all_arrows] assert first_var_name in independent_vars if nonzero_cond: nonzero_cond_vars = [v.swapcase() for v in independent_vars] else: nonzero_cond_vars = [] R = PolynomialRing(QQ, ['M', 'L', 'm', 'l'] + independent_vars + nonzero_cond_vars) M, L, m, l = R('M'), R('L'), R('m'), R('l') def var(tet, edge): return tet_vars[6 * tet + directed_edges.index(edge)] in_terms_of_indep_vars = {v: R(v) for v in independent_vars} in_terms_of_indep_vars['M'] = M in_terms_of_indep_vars['L'] = L edge_gluings = EdgeGluings(gen_obs_class) in_terms_of_indep_vars_data = {v: (1, 0, 0, v) for v in independent_vars} for around_one_edge in arrows_around_edges(manifold): tet0, face0, edge0 = around_one_edge[0] indep_var = R(var(tet0, edge0)) sign, m_e, l_e = 1, 0, 0 for tet1, face1, edge1 in around_one_edge[:-1]: (tet2, face2, edge2), a_sign = edge_gluings[tet1, face1, edge1] sign = a_sign * sign m_e -= sum(m_star[tet1, t3m.TwoSubsimplices[face1], t3m.ZeroSubsimplices[v]] for v in edge1) l_e -= sum(l_star[tet1, t3m.TwoSubsimplices[face1], t3m.ZeroSubsimplices[v]] for v in edge1) mvar = M if m_e > 0 else m lvar = L if l_e > 0 else l dep_var = var(tet2, edge2) in_terms_of_indep_vars_data[dep_var] = (sign, m_e, l_e, var(tet0, edge0)) in_terms_of_indep_vars[dep_var] = sign * (mvar**abs(m_e)) * ( lvar**abs(l_e)) * indep_var tet_vars = [in_terms_of_indep_vars[v] for v in tet_vars] rels = [R(first_var_name) - 1, M * m - 1, L * l - 1] for tet in range(n): a, b, c, d, e, f = tet_vars[6 * tet:6 * (tet + 1)] rels.append(simplify_equation(c * d + a * f - b * e)) # These last equations ensure the ptolemy coordinates are nonzero. # For larger numbers of tetrahedra, this appears to make computing # Groebner basis much faster even though there are extra variables # compared the approach where one uses a single variable. if nonzero_cond: for v in independent_vars: rels.append(R(v) * R(v.swapcase()) - 1) if return_full_var_dict == 'data': return R.ideal(rels), in_terms_of_indep_vars_data if return_full_var_dict: return R.ideal(rels), in_terms_of_indep_vars else: return R.ideal(rels)
def enum_points(I): possibleValues = get_elements() R = I.ring() F = R.base() ch = F.characteristic() n = R.ngens() if n == 0: if I.is_zero(): yield [] return if I.is_one(): return if all(map(lambda _: _.degree() == 1, I.gens())) and (ch > 0 or I.dimension() == 0): # solve using linear algebra f = R.hom(n * [0], F) A = matrix([f(g.coefficient(xi)) for xi in R.gens()] for g in I.gens()) b = vector(-g.constant_coefficient() for g in I.gens()) v0 = A.solve_right(b) r = A.rank() if r == n: yield list(v0) else: K = A.right_kernel().matrix() for v in F**(n - r): yield list(v * K + v0) return if ch > 0 and I.is_homogeneous(): yield [F(0)] * n for pt in enum_proj_points(I): for sca in get_elements(): if sca != 0: yield [x * sca for x in pt] return elim = I.elimination_ideal(I.ring().gens()[1:]) g = elim.gens()[0] if g != 0: S = F['u'] pr1 = R.hom([S.gen()] + [0] * (n - 1), S) possibleValues = (v[0] for v in pr1(g).roots() if bound == None or global_height([v[0], F(1)]) <= bound + tolerance) if split: nonSplit = (f[0] for f in factor(pr1(g)) if f[0].degree() > 1) for f in nonSplit: if ch == 0: F_ = f.splitting_field('a') # `polredbest` from PARI/GP, improves performance significantly f = gen_to_sage( pari(F_.gen().minpoly('x')).polredbest(), {'x': S.gen()}) F_ = f.splitting_field('a') R_ = PolynomialRing(F_, 'x', n) I = R_.ideal( [f.change_ring(base_change(F, F_)) for f in I.gens()]) for pt in enum_points(I): yield pt return R_ = PolynomialRing(F, 'x', n - 1) if n == 1: for v in possibleValues: yield [v] else: for v in possibleValues: pr2 = R.hom([v] + list(R_.gens()), R_) for rest in enum_points(pr2(I)): yield [v] + rest
def rational_points(X, F=None, split=False, bound=None, tolerance=0.01, prec=53): r""" Return an iterator of rational points on a scheme ``X`` INPUT: - ``X`` - a scheme, affine or projective OPTIONS: - ``F`` - coefficient field - ``split`` - whether to compute the splitting field when the scheme is 0-dimensional - ``bound`` - a bound for multiplicative height - ``tolerance`` - tolerance used for computing height - ``prec`` - precision used for computing height OUTPUT: - an iterator of rational points on ``X`` ALGORITHM: Use brute force plus some elimination. The complexity is approximately `O(n^d)`, where `n` is the size of the field (or the number of elements with bounded height when the field is infinite) and `d` is the dimension of the scheme ``X``. Significantly faster than the current available algorithms in Sage, especially for low dimension schemes in large ambient spaces. EXAMPLES:: sage: from rational_points import rational_points A curve of genus 9 over `\mathbf F_{97}` with many rational points (from the website `<https://manypoints.org>`_):: sage: A.<x,y,z> = AffineSpace(GF(97),3) sage: C = A.subscheme([x^4+y^4+1+3*x^2*y^2+13*y^2+68*x^2,z^2+84*y^2+x^2+67]) sage: len(list(rational_points(C))) 228 The following example is from the `documentation <http://magma.maths.usyd.edu.au/magma/handbook/text/1354>`_ of Magma: a space rational curve with one cusp. Unfeasible with the old methods:: sage: F = GF(7823) sage: P.<x,y,z,w> = ProjectiveSpace(F,3) sage: C = P.subscheme([4*x*z+2*x*w+y^2+4*y*w+7821*z^2+7820*w^2,\ ....: 4*x^2+4*x*y+7821*x*w+7822*y^2+7821*y*w+7821*z^2+7819*z*w+7820*w^2]) sage: len(list(rational_points(C))) # long time 7824 31 nodes on a `Togliatti surface <https://en.wikipedia.org/wiki/Togliatti_surface>`_: only 7 of them are defined over the field of definition. Use ``split=True`` to automatically find the splitting field:: sage: q = QQ['q'].0 sage: F.<q> = NumberField(q^4-10*q^2+20) sage: P.<x,y,z,w> = ProjectiveSpace(F,3) sage: f = 5*q*(2*z-q*w)*(4*(x^2+y^2-z^2)+(1+3*(5-q^2))*w^2)^2-64*(x-w)*\ ....: (x^4-4*x^3*w-10*x^2*y^2-4*x^2*w^2+16*x*w^3-20*x*y^2*w+5*y^4+16*w^4-20*y^2*w^2) sage: X = P.subscheme([f]+f.jacobian_ideal()) sage: len(list(rational_points(X))) 7 sage: len(list(rational_points(X,split=True))) 31 Enumeration of points on a projective plane over a number field:: sage: a = QQ['a'].0 sage: F.<a> = NumberField(a^3-5) sage: P.<x,y,z> = ProjectiveSpace(F, 2) sage: len(list(rational_points(P, bound=RR(5^(1/3))))) 49 """ def base_change(k, F): ch = F.characteristic() if ch == 0: return k.embeddings(F)[0] else: return F def enum_proj_points(I): R = I.ring() k = R.base() n = R.ngens() - 1 for i in range(n + 1): R_ = PolynomialRing(k, 'x', n - i) v = [k(0)] * i + [k(1)] pr = R.hom(v + list(R_.gens()), R_) for rest in enum_points(pr(I)): pt = v + rest if bound == None or global_height( pt, prec=prec) <= bound + tolerance: yield pt def enum_points(I): possibleValues = get_elements() R = I.ring() F = R.base() ch = F.characteristic() n = R.ngens() if n == 0: if I.is_zero(): yield [] return if I.is_one(): return if all(map(lambda _: _.degree() == 1, I.gens())) and (ch > 0 or I.dimension() == 0): # solve using linear algebra f = R.hom(n * [0], F) A = matrix([f(g.coefficient(xi)) for xi in R.gens()] for g in I.gens()) b = vector(-g.constant_coefficient() for g in I.gens()) v0 = A.solve_right(b) r = A.rank() if r == n: yield list(v0) else: K = A.right_kernel().matrix() for v in F**(n - r): yield list(v * K + v0) return if ch > 0 and I.is_homogeneous(): yield [F(0)] * n for pt in enum_proj_points(I): for sca in get_elements(): if sca != 0: yield [x * sca for x in pt] return elim = I.elimination_ideal(I.ring().gens()[1:]) g = elim.gens()[0] if g != 0: S = F['u'] pr1 = R.hom([S.gen()] + [0] * (n - 1), S) possibleValues = (v[0] for v in pr1(g).roots() if bound == None or global_height([v[0], F(1)]) <= bound + tolerance) if split: nonSplit = (f[0] for f in factor(pr1(g)) if f[0].degree() > 1) for f in nonSplit: if ch == 0: F_ = f.splitting_field('a') # `polredbest` from PARI/GP, improves performance significantly f = gen_to_sage( pari(F_.gen().minpoly('x')).polredbest(), {'x': S.gen()}) F_ = f.splitting_field('a') R_ = PolynomialRing(F_, 'x', n) I = R_.ideal( [f.change_ring(base_change(F, F_)) for f in I.gens()]) for pt in enum_points(I): yield pt return R_ = PolynomialRing(F, 'x', n - 1) if n == 1: for v in possibleValues: yield [v] else: for v in possibleValues: pr2 = R.hom([v] + list(R_.gens()), R_) for rest in enum_points(pr2(I)): yield [v] + rest ####################################################################### # begin of main function try: I = X.defining_ideal() R = I.ring() except: # when X has no defining ideal, i.e. when it's the whole space R = X.coordinate_ring() I = R.ideal([]) k = R.base() n = R.ngens() ambient = X.ambient_space() if F: # specified coefficient field R_ = PolynomialRing(F, 'x', n) I = R_.ideal([f.change_ring(base_change(k, F)) for f in I.gens()]) k = F ambient = ambient.change_ring(k) split = False ch = k.characteristic() if (X.is_projective() and I.dimension() == 1) or (not X.is_projective() and I.dimension() == 0): # 0-dimensional dimension # in 0-dim use elimination only bound = None get_elements = lambda: [] else: # positive dimension split = False # splitting field does not work in positive dimension if ch == 0: if bound == None: raise ValueError("need to specify a valid bound") if is_RationalField(k): get_elements = lambda: k.range_by_height(floor(bound) + 1) else: get_elements = lambda: k.elements_of_bounded_height( bound=bound**(k.degree()), tolerance=tolerance, precision=prec) else: # finite field bound = None get_elements = lambda: k if X.is_projective(): # projective case for pt in enum_proj_points(I): if split: # TODO construct homogeneous coordinates from a bunch of # elements from different fields yield pt else: yield ambient(pt) else: # affine case for pt in enum_points(I): if split: yield pt else: yield ambient(pt)
def AffineIntegralScheme(R, F, generators): r""" Return an integral affine scheme with given base ring and function field. INPUT: - ``R`` -- an integral domain - ``F`` -- a field, finitely generated over the fraction field of `R` - ``generators`` -- a list of elements of `F` OUTPUT: an affine `R`-scheme `X` with function field `F`, whose coordinate ring is generated over `R` by the given generators. `X` will be equipped with attributes ``_function_field`` (which returns `F`) and ``_generators`` (which returns the list of generators defining `X`). .. NOTE:: At the moment, we assume that `F` is a function field of transcendence degree one over the fraction field of `R`, and that the defining equation for `F` has coefficients in `R`. In particula, the scheme `X` will be an affine arithmetic surface. """ # first we create the ideal defining X n = len(generators) P = AffineSpace(R, n, 'x') A = P.coordinate_ring() if hasattr(F, "polynomial"): # F is a function field over Frac(R) in two variables x, y # with equation G0(x,y)=0. Write the generators as # f_i =g_i(x,y)/h_i(x,y) and define the ideal generated by # G0 and h_i(x,y)*z_i - g_i(x,y). We obtain the ideal defining # X by eliminating x, y from these equations. B = PolynomialRing(R, n + 2, 'z') x = B.gens()[n] y = B.gens()[n + 1] G0 = function_field_equation(R, F)(x, y) G = [G0] for i in range(n): f = generators[i] # we have to write f = g/h, where g, h are elements of B g, h = function_field_element(f, R) g = g(x, y) h = h(x, y) G.append(h * B.gens()[i] - g) J = B.ideal(G).elimination_ideal([x, y]) # in general, this is not a prime ideal; we have to first # replace it by its radical. Probably we need to write a # Singular script for this G = [] for g in J.gens(): G.append(g(A.gens() + (A.zero(), A.zero()))) else: # F is a rational function field over Frac(R) B = PolynomialRing(R, n + 1, 'z') x = B.gens()[n] G = [] for i in range(n): f = generators[i] g = f.numerator() a = common_denominator_of_polynomial(g, R) h = f.denominator() b = common_denominator_of_polynomial(h, R) c = lcm(a, b) g = c * g h = c * h G.append(h(x) * B.gens()[i] - g(x)) J = B.ideal(G).elimination_ideal([x]) G = [] for g in J.gens(): G.append(g(A.gens() + tuple([A.zero()]))) J_list = horizontal_minimal_ass_primes(A.ideal(G)) assert len( J_list ) == 1, "some thing strange happened: there is more than one horizontal pime ideal" X = P.subscheme(J_list[0]) X._function_field = F X._generators = generators return X