def radius(P): r"""Maximum norm of any element in a polyhedron, with respect to the supremum norm. It is defined as `\max\limits_{x in P} \Vert x \Vert_\inf`. It can be computed with support functions as `\max\limits_{i=1,\ldots,n} \max \{|\rho_P(e_i)|, |\rho_P(-e_i)|\}`, where `\rho_P(e_i)` is the support function of `P` evaluated at the i-th canonical vector `e_i`. INPUT: * ``P`` - an object of class Polyhedron. It can also be given as ``[A, b]`` with A and b matrices, assuming `Ax \leq b`. OUTPUT: * ``snorm`` - the value of the max norm of any element of P, in the sup-norm. TO-DO: - To use '*args' so that it works both with ``polyhedron_sup_norm(A, b)`` and with ``polyhedron_sup_norm(P)`` (depending on the number of the arguments). - The difference with mult-methods is that they check also for the type of the arguments. - v = P.bounding_box() # returns the coordinates of a rectangular box containing the polytope polyInfNorm = max( vecpnorm(v[i],p='inf') for i in range(len(v))) return polyInfNorm """ from polyhedron_tools.misc import support_function if (type(P) == list): A = P[0] b = P[1] # obtain dimension of the ambient space n = A.ncols() r = 0 for i in range(n): # generate canonical direction d = zero_vector(RDF, n) d[i] = 1 aux_sf = abs(support_function([A, b], d)) if (aux_sf >= r): r = aux_sf # change sign d[i] = -1 aux_sf = abs(support_function([A, b], d)) if (aux_sf >= r): r = aux_sf snorm = r return snorm
def zero(self): """returns a distribution with all zero moments (and the same prime and weight)""" return dist(self.p,self.weight,zero_vector(QQ,self.num_moments()))
def zero(self,S): ''' encoding of 0 at index S ''' return self.encode(zero_vector(self.n), S)
def _find_isomorphism_degenerate(self, polytope): """ Helper to pick an isomorphism of degenerate polygons INPUT: - ``polytope`` -- a :class:`LatticePolytope_PPL_class`. The polytope to compare with. EXAMPLES:: sage: from sage.geometry.polyhedron.ppl_lattice_polytope import LatticePolytope_PPL, C_Polyhedron sage: L1 = LatticePolytope_PPL(C_Polyhedron(2, 'empty')) sage: L2 = LatticePolytope_PPL(C_Polyhedron(3, 'empty')) sage: iso = L1.find_isomorphism(L2) # indirect doctest sage: iso(L1) == L2 True sage: iso = L1._find_isomorphism_degenerate(L2) sage: iso(L1) == L2 True sage: L1 = LatticePolytope_PPL((-1,4)) sage: L2 = LatticePolytope_PPL((2,1,5)) sage: iso = L1.find_isomorphism(L2) sage: iso(L1) == L2 True sage: L1 = LatticePolytope_PPL((-1,), (3,)) sage: L2 = LatticePolytope_PPL((2,1,5), (2,-3,5)) sage: iso = L1.find_isomorphism(L2) sage: iso(L1) == L2 True sage: L1 = LatticePolytope_PPL((-1,-1), (3,-1)) sage: L2 = LatticePolytope_PPL((2,1,5), (2,-3,5)) sage: iso = L1.find_isomorphism(L2) sage: iso(L1) == L2 True sage: L1 = LatticePolytope_PPL((-1,2), (3,1)) sage: L2 = LatticePolytope_PPL((1,2,3),(1,2,4)) sage: iso = L1.find_isomorphism(L2) sage: iso(L1) == L2 True sage: L1 = LatticePolytope_PPL((-1,2), (3,2)) sage: L2 = LatticePolytope_PPL((1,2,3),(1,2,4)) sage: L1.find_isomorphism(L2) Traceback (most recent call last): ... LatticePolytopesNotIsomorphicError: different number of integral points sage: L1 = LatticePolytope_PPL((-1,2), (3,1)) sage: L2 = LatticePolytope_PPL((1,2,3),(1,2,5)) sage: L1.find_isomorphism(L2) Traceback (most recent call last): ... LatticePolytopesNotIsomorphicError: different number of integral points """ from sage.geometry.polyhedron.lattice_euclidean_group_element import \ LatticePolytopesNotIsomorphicError polytope_vertices = polytope.vertices() self_vertices = self.ordered_vertices() # handle degenerate cases if self.n_vertices() == 0: A = zero_matrix(ZZ, polytope.space_dimension(), self.space_dimension()) b = zero_vector(ZZ, polytope.space_dimension()) return LatticeEuclideanGroupElement(A, b) if self.n_vertices() == 1: A = zero_matrix(ZZ, polytope.space_dimension(), self.space_dimension()) b = polytope_vertices[0] return LatticeEuclideanGroupElement(A, b) if self.n_vertices() == 2: self_origin = self_vertices[0] self_ray = self_vertices[1] - self_origin polytope_origin = polytope_vertices[0] polytope_ray = polytope_vertices[1] - polytope_origin Ds, Us, Vs = self_ray.column().smith_form() Dp, Up, Vp = polytope_ray.column().smith_form() assert Vs.nrows() == Vs.ncols() == Vp.nrows() == Vp.ncols() == 1 assert abs(Vs[0, 0]) == abs(Vp[0, 0]) == 1 A = zero_matrix(ZZ, Dp.nrows(), Ds.nrows()) A[0, 0] = 1 A = Up.inverse() * A * Us * (Vs[0, 0] * Vp[0, 0]) b = polytope_origin - A * self_origin try: A = matrix(ZZ, A) b = vector(ZZ, b) except TypeError: raise LatticePolytopesNotIsomorphicError('different lattice') hom = LatticeEuclideanGroupElement(A, b) if hom(self) == polytope: return hom raise LatticePolytopesNotIsomorphicError('different polygons')
def BoxInfty(lengths=None, center=None, radius=None, base_ring=QQ, return_HSpaceRep=False): r"""Generate a ball in the supremum norm, or more generally a hyper-rectangle in an Euclidean space. It can be constructed from its center and radius, in which case it is a box. It can also be constructed giving the lengths of the sides, in which case it is an hyper-rectangle. In all cases, it is defined as the Cartesian product of intervals. INPUT: * ``args`` - Available options are: * by center and radius: * ``center`` - a vector (or a list) containing the coordinates of the center of the ball. * ``radius`` - a number representing the radius of the ball. * by lengths: * ``lenghts`` - a list of tuples containing the length of each side with respect to the coordinate axes, in the form [(min_x1, max_x1), ..., (min_xn, max_xn)]. * ``base_ring`` - (default: ``QQ``) base ring passed to the Polyhedron constructor. Valid choices are: * ``'QQ'``: rational * ``'RDF'``: real double field * ``return_HSpaceRep`` - (default: False) If True, it does not construct the Polyhedron `P`, and returns instead the pairs ``[A, b]`` corresponding to P in half-space representation, and is understood that `Ax \leq b`. OUTPUT: * ``P`` - a Polyhedron object. If the flag ``return_HSpaceRep`` is true, it is returned as ``[A, b]`` with `A` and `b` matrices, and is understood that `Ax \leq b`. EXAMPLES:: sage: from polyhedron_tools.misc import BoxInfty sage: P = BoxInfty([1,2,3], 1); P A 3-dimensional polyhedron in QQ^3 defined as the convex hull of 8 vertices sage: P.plot(aspect_ratio=1) # not tested (plot) NOTES: - The possibility to output in matrix form [A, b] is especially interesting for more than 15 variable systems. """ # Guess input got_lengths, got_center_and_radius = False, False if (lengths is not None): if (center is None) and (radius is None): got_lengths = True elif (center is not None) and (radius is None): radius = center center = lengths lengths = [] got_center_and_radius = True else: got_center_and_radius = True if (center is not None and radius is not None) else False # Given center and radius if got_center_and_radius: # cast (optional) center = [base_ring(xi) for xi in center] radius = base_ring(radius) ndim = len(center) A = matrix(base_ring, 2 * ndim, ndim) b = vector(base_ring, 2 * ndim) count = 0 for i in range(ndim): diri = zero_vector(base_ring, ndim) diri[i] = 1 # external bound A.set_row(count, diri) b[count] = center[i] + radius count += 1 # internal bound A.set_row(count, -diri) b[count] = -(center[i] - radius) count += 1 # Given the length of each side as tuples (min, max) elif got_lengths: # clean up the argument and cast lengths = _make_listlist(lengths) lengths = [[base_ring(xi) for xi in l] for l in lengths] ndim = len(lengths) A = matrix(base_ring, 2 * ndim, ndim) b = vector(base_ring, 2 * ndim) count = 0 for i in range(ndim): diri = zero_vector(base_ring, ndim) diri[i] = 1 # external bound A.set_row(count, diri) b[count] = lengths[i][1] count += 1 # internal bound A.set_row(count, -diri) b[count] = -(lengths[i][0]) count += 1 else: raise ValueError( 'You should specify either center and radius or length of the sides.' ) if not return_HSpaceRep: P = polyhedron_from_Hrep(A, b, base_ring) return P else: return [A, b]
def _plot(self, geosub, color=None): r""" EXAMPLES:: sage: from EkEkstar import kFace, GeoSub sage: sub = {1:[1,2], 2:[1,3], 3:[1]} sage: geosub = GeoSub(sub,2, dual=True) sage: _ = kFace((10,21,33), (1,))._plot(geosub) # case A sage: _ = kFace((10,21,33), (1,2), dual=True)._plot(geosub) # case C sage: _ = kFace((10,21,33), (1,), dual=True)._plot(geosub) # case E :: sage: sub = {1: [1, 3, 2, 3], 2: [2, 3], 3: [3, 2, 3, 1, 3, 2, 3]} sage: geosub = GeoSub(sub, 2, dual=True) sage: _ = kFace((10,21,33), (1,2), dual=True)._plot(geosub) # case B :: sage: sub = {1:[1,2,3,3,3,3], 2:[1,3], 3:[1]} sage: geosub = GeoSub(sub,2, dual=True) sage: _ = kFace((0,0,0),(1,2), dual=True)._plot(geosub) # case A """ v = self.vector() t = self.type() if color != None: col = color else: col = self._color G = Graphics() K = geosub.field() b = K.gen() num = geosub._sigma_dict.keys() if self.is_dual(): h = list(set(num) - set(t)) B = b vec = geosub.dominant_left_eigenvector() emb = geosub.contracting_eigenvalues_indices() else: h = list(t) B = b**(-1) # TODO: this seems useless (why?) vec = -geosub.dominant_left_eigenvector() emb = geosub.dilating_eigenvalues_indices() el = v * vec iter = 0 conjugates = geosub.complex_embeddings() if len(h) == 1: if conjugates[emb[0]].is_real() == True: bp = zero_vector(CC, len(emb)) for i in range(len(emb)): bp[i] = K(el).complex_embeddings()[emb[i]] bp1 = zero_vector(CC, len(emb)) for i in range(len(emb)): bp1[i] = K( (el + vec[h[0] - 1])).complex_embeddings()[emb[i]] if len(emb) == 1: return line([bp[0], bp1[0]], color=col, thickness=3) else: return line([bp, bp1], color=col, thickness=3) else: bp = K(el).complex_embeddings()[emb[0]] bp1 = K((el + vec[h[0] - 1])).complex_embeddings()[emb[0]] return line([bp, bp1], color=col, thickness=3) elif len(h) == 2: if conjugates[emb[0]].is_real() == True: bp = (K(el).complex_embeddings()[emb[0]], K(el).complex_embeddings()[emb[1]]) bp1 = (K(el + vec[h[0] - 1]).complex_embeddings()[emb[0]], K(el + vec[h[0] - 1]).complex_embeddings()[emb[1]]) bp2 = (K(el + vec[h[0] - 1] + vec[h[1] - 1]).complex_embeddings()[emb[0]], K(el + vec[h[0] - 1] + vec[h[1] - 1]).complex_embeddings()[emb[1]]) bp3 = (K(el + vec[h[1] - 1]).complex_embeddings()[emb[0]], K(el + vec[h[1] - 1]).complex_embeddings()[emb[1]]) return polygon2d([bp, bp1, bp2, bp3], color=col, thickness=.1, alpha=0.8) else: bp = K(el).complex_embeddings()[emb[0]] bp1 = K(el + vec[h[0] - 1]).complex_embeddings()[emb[0]] bp2 = K(el + vec[h[0] - 1] + vec[h[1] - 1]).complex_embeddings()[emb[0]] bp3 = K(el + vec[h[1] - 1]).complex_embeddings()[emb[0]] return polygon2d([[bp[0], bp[1]], [bp1[0], bp1[1]], [bp2[0], bp2[1]], [bp3[0], bp3[1]]], color=col, thickness=.1, alpha=0.8) else: raise NotImplementedError( "Plotting is implemented only for patches in two or three dimensions." ) return G
def milton_proj(self, geosub): r""" EXAMPLES:: sage: from EkEkstar import kFace, GeoSub sage: sub = {1:[1,2], 2:[1,3], 3:[1]} sage: geosub = GeoSub(sub,2, dual=True) sage: kFace((10,21,33), (1,2), dual=True).milton_proj(geosub) # case C [-45.2833796391680 + 24.0675974519667*I, -46.0552241455140 + 25.1827399600067*I] sage: kFace((10,21,33), (1,), dual=True).milton_proj(geosub) # case E [[-45.2833796391680, 24.0675974519667], [-46.7030230167750, 23.4613067227595], [-47.4748675231211, 24.5764492307995], [-46.0552241455140, 25.1827399600067]] Brun substitutions ``[123,132,213,231]`` gives a incidence matrix with totally real eigenvalues:: sage: from slabbe.mult_cont_frac import Brun sage: algo = Brun() sage: S = algo.substitutions() sage: sub = prod([S[a] for a in [123,132,213,231]]) sage: sub WordMorphism: 1->1323, 2->23, 3->3231323 Case B:: sage: sub = {1: [1,3,2,3], 2: [2,3], 3: [3,2,3,1,3,2,3]} sage: geosub = GeoSub(sub, 2, dual=True) sage: kFace((10,21,33), (1,2), dual=True).milton_proj(geosub) # case B [(6.69036552922519, -1.50019003695006), (5.44338592550772, -1.05514816903743)] Case A:: sage: sub = {1:[1,2,3,3,3,3], 2:[1,3], 3:[1]} sage: geosub = GeoSub(sub,2, dual=True) sage: kFace((0,0,0),(1,2), dual=True).milton_proj(geosub) # case A [0.000000000000000, -4.74482607768192] Case D:: sage: #kFace((10,21,33), (1,2)).milton_proj(geosub) # case D (broken) """ v = self.vector() t = self.type() K = geosub.field() b = K.gen() num = geosub._sigma_dict.keys() if self.is_dual(): h = list(set(num) - set(t)) B = b vec = geosub.dominant_left_eigenvector() emb = geosub.contracting_eigenvalues_indices() else: h = list(t) B = b**(-1) # TODO: this seems useless (why?) vec = -geosub.dominant_left_eigenvector() emb = geosub.dilating_eigenvalues_indices() el = v * vec iter = 0 conjugates = geosub.complex_embeddings() if len(h) == 1: if conjugates[emb[0]].is_real() == True: bp = zero_vector(CC, len(emb)) for i in range(len(emb)): bp[i] = K(el).complex_embeddings()[emb[i]] bp1 = zero_vector(CC, len(emb)) for i in range(len(emb)): bp1[i] = K( (el + vec[h[0] - 1])).complex_embeddings()[emb[i]] if len(emb) == 1: #print "case A" return [bp[0], bp1[0]] else: #print "case B" return [bp, bp1] else: bp = K(el).complex_embeddings()[emb[0]] bp1 = K((el + vec[h[0] - 1])).complex_embeddings()[emb[0]] #print "case C" return [bp, bp1] elif len(h) == 2: if conjugates[emb[0]].is_real() == True: bp = (K(el).complex_embeddings()[emb[0]], K(el).complex_embeddings()[emb[1]]) bp1 = (K(el + vec[h[0] - 1]).complex_embeddings()[emb[0]], K(el + vec[h[0] - 1]).complex_embeddings()[emb[1]]) bp2 = (K(el + vec[h[0] - 1] + vec[h[1] - 1]).complex_embeddings()[emb[0]], K(el + vec[h[0] - 1] + vec[h[1] - 1]).complex_embeddings()[emb[1]]) bp3 = (K(el + vec[h[1] - 1]).complex_embeddings()[emb[0]], K(el + vec[h[1] - 1]).complex_embeddings()[emb[1]]) #print "case D" return [bp, bp1, bp2, bp3] else: bp = K(el).complex_embeddings()[emb[0]] bp1 = K(el + vec[h[0] - 1]).complex_embeddings()[emb[0]] bp2 = K(el + vec[h[0] - 1] + vec[h[1] - 1]).complex_embeddings()[emb[0]] bp3 = K(el + vec[h[1] - 1]).complex_embeddings()[emb[0]] #print "case E" return [[bp[0], bp[1]], [bp1[0], bp1[1]], [bp2[0], bp2[1]], [bp3[0], bp3[1]]] else: raise NotImplementedError( "Projection is implemented only for patches in two or three dimensions." )
def asphericity_polytope(P, tol=1e-1, MaxIter=1e2, verbose=0, solver='GLPK'): r""" Algorithm for computing asphericity INPUT: ``x0`` - point in the interior of P ``tol`` - stop condition OUTPUT: ``psi_opt`` - `\min_{x \in P} psi(x) = R(x)/r(x)`, where `R(x)` and `r(x)` are the circumradius and the inradius at `x` respectively ``alphakList`` - sequence of `\psi(x)` which converges to `\psi_{\text{opt}}` ``xkList`` - sequence of interior points which converge to the minimum """ # check if the polytope is not full-dimensional -- in this case the notion # of asphericity still makes sense but there is yet no "affine function" (or # "relative interior") method from the Polyhedron class that we can use # see :trac:16045 if not P.is_full_dimensional(): raise NotImplementedError('The polytope is not full-dimensional') # initial data p = P.ambient_dim() [DP, d] = polyhedron_to_Hrep(P) # in the form Dy <= d D = -DP # transform to <Dj,y> + dj >= 0 l = D.nrows() mP = opposite_polyhedron(P) # unit ball, infinity norm Bn = BoxInfty(center=zero_vector(RDF, p), radius=1) [C, c] = polyhedron_to_Hrep(Bn) C = -C # transform to <Cj,y> + cj >= 0 m = len(c) # auxiliary matrices A, a, B, b A = matrix(RDF, C.nrows(), C.ncols()) a = [] for i in range(m): A.set_row(i, -C.row(i) / c[i]) a.append(support_function(mP, A.row(i), solver=solver)) B = matrix(RDF, D.nrows(), D.ncols()) b = [] for j in range(l): [nDj, ymax] = support_function(Bn, D.row(j), return_xopt=True, solver=solver) B.set_row(j, D.row(j) / nDj) b.append(d[j] / nDj) # generate an initial point x0 = chebyshev_center(DP, d) # compute asphericity at x0 R = max(A.row(i) * vector(x0) + a[i] for i in range(m)) r = min(B.row(j) * vector(x0) + b[j] for j in range(l)) alpha0 = R / r xkList = [vector(x0)] alphakList = [alpha0] xk0 = x0 psi_k0 = alpha0 alphak0 = alpha0 convergeFlag = 0 iterCount = 0 while (iterCount < MaxIter) and (not convergeFlag): [psi_k, xkDict, zkDict] = _asphericity_iteration(xk0, l, m, p, A, a, B, b, verbose=verbose, solver=solver) xk = vector(xkDict) xkList.append(xk) zk = vector(zkDict) alphakList.append(zk[0]) xk0 = xk if (abs(psi_k - psi_k0) < tol): convergeFlag = 1 psi_k0 = psi_k iterCount += 1 x_asph = xkList.pop() R_asph = max(A.row(i) * vector(x_asph) + a[i] for i in range(m)) r_asph = min(B.row(j) * vector(x_asph) + b[j] for j in range(l)) asph = R_asph / r_asph return [asph, x_asph]
def __init__(self, N): r""" INPUT: ``N`` -- a positive integer OUTPUT: none EXAMPLES: :: """ ## Store the level self.__N = N ## Creates and stores the Sage representation of P^1(Z/NZ) P = P1List(N) self.__P = P ## Creates a fundamental domain for Gamma_0(N) whose boundary is a union ## of unimodular paths (except in the case of 3-torsion). ## We will call the intersection of this domain with the real axis the ## collection of cusps (even if some are Gamma_0(N) equivalent to one another). cusps = self.form_list_of_cusps() ## Takes the boundary of this fundamental domain and finds SL_2(Z) matrices whose ## associated unimodular path gives this boundary. These matrices form the ## beginning of our collection of coset reps for Gamma_0(N) / SL_2(Z). coset_reps = self.fd_boundary(cusps) ## Make the identity matrix Id = Matrix(2,2,[1,0,0,1]) ## Gives names to matrices of order 2 and 3 (in PSL_2) sig = Matrix(2,2,[0,1,-1,0]) tau = Matrix(2,2,[0,-1,1,-1]) ## Takes the bottom row of each of our current coset reps, ## thinking of them as distinct elements of P^1(Z/NZ) p1s = [(coset_reps[j])[1] for j in range(len(coset_reps))] ## Initializes relevant Manin data gens = [] twotor = [] twotorrels = [] threetor = [] threetorrels = [] rels = [0 for i in range(0,len(coset_reps))] ## the list rels (above) will give Z[Gamma_0(N)] relations between ## the associated divisor of each coset representatives in terms ## of our chosen set of generators. ## entries of rel will be lists of elements of the form (c,A,r) ## with c a constant, A a Gamma_0(N) matrix, and r the index of a ## generator. The meaning is that the divisor associated to the ## j-th coset rep will equal the sum of: ## ## c * A^(-1) * (divisor associated to r-th coset rep) ## ## as one varies over all (c,A,r) in rel[j]. ## (Here r must be in self.generator_indices().) ## ## This will be used for modular symbols as then the value of a ## modular symbol phi on the (associated divisor) of the j-th ## element of coset_reps will be the sum of c * phi (r-th genetator) | A ## as one varies over the tuples in rel[j] boundary_checked = [False for i in range(0,len(coset_reps))] ## The list boundary_checked keeps track of which boundary pieces of the ## fundamental domain have been already used as we are picking ## our generators # glue_data = [0 for i in range(0,len(coset_reps))] ## ???? ## The following loop will choose our generators by picking one edge ## out of each pair of edges that are glued to each other and picking ## each edge glued to itself (arising from two-torsion) ## ------------------------------------------------------------------ for r in range(0,len(coset_reps)): if boundary_checked[r] == False: ## We now check if this boundary edge is glued to itself by ## Gamma_0(N) if P.index(p1s[r][0],p1s[r][1]) == P.index(-p1s[r][1],p1s[r][0]): ## This edge is glued to itself and so coset_reps[r] ## needs to be added to our generator list. rels[r] = [(1,Id,r)] ## this relation expresses the fact ## that coset_reps[r] is one of our ## basic generators gens = gens + [r] ## the index r is adding to our list ## of indexes of generators twotor = twotor + [r] ## the index r is adding to our ## list of indexes of generators ## which satisfy a 2-torsion relation gam = coset_reps[r] * sig * coset_reps[r]**(-1) ## gam is 2-torsion matrix and in Gamma_0(N). ## if D is the divisor associated to coset_reps[r] ## then gam * D = - D and so (1+gam)D=0. ## This gives a restriction to the possible values of ## modular symbols on D twotorrels = twotorrels + [gam] ## The 2-torsion matrix gam is recorded in our list of ## 2-torsion relations. # glue_data[r]=(r,gam) ## this records that the edge associated to coset_reps[r] ##is glued to itself by gam boundary_checked[r] = True ## We have now finished with this edge. else: c = coset_reps[r][1,0] d = coset_reps[r][1,1] if (c**2+d**2+c*d)%N == 0: ## If true, then the ideal triangle below the unimodular ## path described by coset_reps[r] contains a point ## fixed by a 3-torsion element. gens = gens + [r] ## the index r is adding to our list of indexes ## of generators rels[r] = [(1,Id,r)] ## this relation expresses the fact that coset_reps[r] ## is one of our basic generators threetor = threetor + [r] ## the index r is adding to our list of indexes of ##generators which satisfy a 3-torsion relation gam = coset_reps[r] * tau * coset_reps[r]**(-1) ## gam is 3-torsion matrix and in Gamma_0(N). ## if D is the divisor associated to coset_reps[r] ## then (1+gam+gam^2)D=0. ## This gives a restriction to the possible values of ## modular symbols on D threetorrels = threetorrels + [gam] ## The 3-torsion matrix gam is recorded in our list of ## 2-torsion relations. ### DID I EVER ADD THE GLUE DATA HERE? DO I NEED IT? ## The reverse of the unimodular path associated to ## coset_reps[r] is not Gamma_0(N) equivalent to it, so ## we need to include it in our list of coset ## representatives and record the relevant relations. a = coset_reps[r][0][0] b = coset_reps[r][0][1] A = Matrix(2,2,[-b,a,-d,c]) coset_reps = coset_reps + [A] ## A (representing the reversed edge) is included in ## our list of coset reps rels = rels + [[(-1,Id,r)]] ## This relation means that phi on the reversed edge ## equals -phi on original edge boundary_checked[r] = True ## We have now finished with this edge. else: ## This is the generic case where neither 2 or ## 3-torsion intervenes. ## The below loop searches through the remaining edges ## and finds which one is equivalent to the reverse of ## coset_reps[r] ## --------------------------------------------------- found = False s = r + 1 ## s is the index we use to run through the ## remaining edges while (not found): if P.index(p1s[s][0],p1s[s][1]) == P.index(-p1s[r][1],p1s[r][0]): ## the reverse of coset_reps[r] is ## Gamma_0(N)-equivalent to coset_reps[s] ## coset_reps[r] will now be made a generator ## and we need to express phi(coset_reps[s]) ## in terms of phi(coset_reps[r]) gens = gens + [r] ## the index r is adding to our list of ## indexes of generators rels[r] = [(1,Id,r)] ## this relation expresses the fact that ## coset_reps[r] is one of our basic generators A = coset_reps[s] * sig ## A corresponds to reversing the orientation ## of the edge corr. to coset_reps[r] gam = coset_reps[r] * A**(-1) ## gam is in Gamma_0(N) (by assumption of ## ending up here in this if statement) rels[s] = [(-1,gam,r)] ## this relation means that phi evaluated on ## coset_reps[s] equals -phi(coset_reps[r])|gam ## To see this, let D_r be the divisor ## associated to coset_reps[r] and D_s to ## coset_reps[s]. Then gam D_s = -D_r and so ## phi(gam D_s) = - phi(D_r) and thus ## phi(D_s) = -phi(D_r)|gam ## since gam is in Gamma_0(N) # glue_data[r] = (s,gam) ## ???? found = True boundary_checked[r] = 1 boundary_checked[s] = 1 else: s = s + 1 ## moves on to the next edge ## We now need to complete our list of coset representatives by ## finding all unimodular paths in the interior of the fundamental ## domain, as well as express these paths in terms of our chosen set ## of generators. ## ------------------------------------------------------------------- for r in range(len(cusps)-2): ## r is the index of the cusp on the left of the path. We only run ## thru to the number of cusps - 2 since you can't start an interior ## path on either of the last two cusps for s in range(r+2,len(cusps)): ## s is in the index of the cusp on the the right of the path cusp1 = cusps[r] cusp2 = cusps[s] if self.is_unimodular_path(cusp1,cusp2): A,B = self.unimod_to_matrices(cusp1,cusp2) ## A and B are the matrices whose associated paths ## connect cusp1 to cusp2 and cusp2 to cusp1 (respectively) coset_reps = coset_reps + [A,B] ## A and B are added to our coset reps vA = [] vB = [] ## This loop now encodes the relation between the ## unimodular path A and our generators. This is done ## simply by accounting for all of the edges that lie ## below the path attached to A (as they form a triangle) ## Similarly, this is also done for B. for j in range(s-r): ## Running between the cusps between cusp1 and cusp2 vA = vA + rels[r+j+2] ## Edge relation added t = (-rels[r+j+2][0][0],rels[r+j+2][0][1],rels[r+j+2][0][2]) ## This is simply the negative of the above edge relation. vB = vB + [t] ## Negative of edge relation added rels = rels + [vA,vB] ## Relations for A and B adding to relations list #### return [coset_reps,gens,twotor,twotorrels,threetor,threetorrels,rels,glue_data] ## Store the data coming from solving the Manin Relations ## ====================================================== self.__mats = coset_reps ## Coset representatives of Gamma_0(N) coming from the geometric ## fundamental domain algorithm ## Make the translation table between the Sage and Geometric ## descriptions of P^1 v = zero_vector(len(coset_reps)) for r in range(len(coset_reps)): v[P.index(coset_reps[r][1,0], coset_reps[r][1,1])] = r self.__P1_to_mats = v self.__gens = gens ## This is a list of indices of the (geometric) coset representatives ## whose values (on the associated degree zero divisors) determine the ## modular symbol. self.__twotor = twotor ## A list of indices of the (geometric) coset representatives whose ## paths are identified by some 2-torsion element (which switches the ## path orientation) self.__twotorrels = twotorrels ## A list of (2-torsion in PSL_2(Z)) matrices in Gamma_0(N) that give ## the orientation identification in the paths listed in twotor above! self.__threetor = threetor ## A list of indices of the (geometric) coset representatives that ## form one side of an ideal triangle with an interior fixed point of ## a 3-torsion element of Gamma_0(N) self.__threetorrels = threetorrels ## A list of (3-torsion in PSL_2(Z)) matrices in Gamma_0(N) that give ## the interior fixed point described in threetor above! self.__rels = rels
def compute_flowpipe(A=None, X0=None, B=None, U=None, **kwargs): r"""Implements LGG reachability algorithm for the linear continuous system dx/dx = Ax + Bu. INPUTS: * ``A`` -- coefficient matrix of the system * ``X0`` -- initial set * ``B`` -- transformation of the input * ``U`` -- input set * ``time_step`` -- (default = 1e-2) time step * ``initial_time`` -- (default = 0) the initial time * ``time_horizon`` -- (default = 1) the final time * ``number_of_time_steps`` -- (default = ceil(T/tau)) number of time steps * "directions" -- (default: random, and a box) dictionary * ``solver`` -- LP solver. Valid options are: * 'GLPK' (default). * 'Gurobi' * ``base_ring`` -- base ring where polyhedral computations are performed Valid options are: * QQ - (default) rational field * RDF - real double field OUTPUTS: * ``flowpipe`` """ # ################ # Parse input # # ################ if A is None: raise ValueError('System matrix A is missing.') else: if 'sage.matrix' in str(type(A)): n = A.ncols() elif type(A) == np.ndarray: n = A.shape[0] base_ring = kwargs['base_ring'] if 'base_ring' in kwargs else QQ if X0 is None: raise ValueError('Initial state X0 is missing.') elif 'sage.geometry.polyhedron' not in str(type(X0)) and type(X0) == list: # If X0 is not some type of polyhedron, set an initial point X0 = Polyhedron(vertices = [X0], base_ring = base_ring) elif 'sage.geometry.polyhedron' not in str(type(X0)) and X0.is_vector(): X0 = Polyhedron(vertices = [X0], base_ring = base_ring) elif 'sage.geometry.polyhedron' in str(type(X0)): # ensure that all input sets are on the same ring # not sure about this if 1==0: if X0.base_ring() != base_ring: [F, g] = polyhedron_to_Hrep(X0) X0 = polyhedron_from_Hrep(F, g, base_ring=base_ring) else: raise ValueError('Initial state X0 not understood') if B is None: # the system is homogeneous: dx/dt = Ax got_homogeneous = True else: got_homogeneous = False if U is None: raise ValueError('Input range U is missing.') tau = kwargs['time_step'] if 'time_step' in kwargs else 1e-2 t0 = kwargs['initial_time'] if 'initial_time' in kwargs else 0 T = kwargs['time_horizon'] if 'time_horizon' in kwargs else 1 global N N = kwargs['number_of_time_steps'] if 'number_of_time_steps' in kwargs else ceil(T/tau) directions = kwargs['directions'] if 'directions' in kwargs else {'select':'box'} global solver solver = kwargs['solver'] if 'solver' in kwargs else 'GLPK' global verbose verbose = kwargs['verbose'] if 'verbose' in kwargs else 0 # this involves the convex hull of X0 and a Minkowski sum #first_element_evaluation = kwargs['first_element_evaluation'] if 'first_element_evaluation' in kwargs else 'approximate' # ####################################################### # Generate template directions # # ####################################################### if directions['select'] == 'box': if n==2: theta = [0,pi/2,pi,3*pi/2] # box dList = [vector(RR,[cos(t), sin(t)]) for t in theta] else: # directions of hypercube dList = [] dList += [-identity_matrix(n).column(i) for i in range(n)] dList += [identity_matrix(n).column(i) for i in range(n)] elif directions['select'] == 'oct': if n != 2: raise NotImplementedError('Directions select octagon not implemented for n other than 2. Try box.') theta = [i*pi/4 for i in range(8)] # octagon dList = [vector(RR,[cos(t), sin(t)]) for t in theta] elif directions['select'] == 'random': order = directions['order'] if 'order' in directions else 12 if n == 2: theta = [random.uniform(0, 2*pi.n(digits=5)) for i in range(order)] dList = [vector(RR,[cos(theta[i]), sin(theta[i])]) for i in range(order)] else: raise NotImplementedError('Directions select random not implemented for n greater than 2. Try box.') elif directions['select'] == 'custom': dList = directions['dList'] else: raise TypeError('Template directions not understood.') # transform directions to numpy array, and get number of directions dArray = np.array(dList) k = len(dArray) global Phi_tau, expX0, alpha_tau_B if got_homogeneous: # dx/dx = Ax # ####################################################### # Compute first element of the approximating sequence # # ####################################################### # compute matrix exponential exp(A*tau) Phi_tau = expm(np.multiply(A, tau)) # compute exp(tau*A)X0 expX0 = Phi_tau * X0 # compute the bloating factor Ainfty = A.norm(Infinity) RX0 = radius(X0) unitBall = BoxInfty(center = zero_vector(n), radius = 1, base_ring = base_ring) alpha_tau = (exp(tau*Ainfty) - 1 - tau*Ainfty)*(RX0) alpha_tau_B = (alpha_tau*np.identity(n)) * unitBall # now we have that: # Omega0 = X0.convex_hull(expX0.Minkowski_sum(alpha_tau_B)) # compute the first element of the approximating sequence, Omega_0 #if first_element_evaluation == 'exact': # Omega0 = X0.convex_hull(expX0.Minkowski_sum(alpha_tau_B)) #elif first_element_evaluation == 'approximate': # NOT TESTED!!! #Omega0_A = dArray # Omega0_b = np.zeros(k) # for i, d in enumerate(dArray): # rho_X0_d = supp_fun_polyhedron(X0, d, solver=solver, verbose=verbose) # rho_expX0_d = supp_fun_polyhedron(expX0, d, solver=solver, verbose=verbose) # rho_alpha_tau_B_d = supp_fun_polyhedron(alpha_tau_B, d, solver=solver, verbose=verbose) # Omega0_b[i] = max(rho_X0_d, rho_expX0_d + rho_alpha_tau_B_d); # Omega0 = PolyhedronFromHSpaceRep(dArray, Omega0_b); #W_tau = Polyhedron(vertices = [], ambient_dim=n) # since W_tau = [], supp_fun_polyhedron returns 0 # ################################################ # Build the sequence of approximations Omega_i # # ################################################ Omega_i_Family_SF = [_Omega_i_supports_hom(d, X0) for d in dArray] else: # dx/dx = Ax + Bu global tau_V, beta_tau_B # compute range of the input under B, V = BU V = B * U # compute matrix exponential exp(A*tau) Phi_tau = expm(np.multiply(A, tau)) # compute exp(tau*A)X0 expX0 = Phi_tau * X0 # compute the initial over-approximation tau_V = (tau*np.identity(n)) * V # compute the bloating factor Ainfty = A.norm(Infinity) RX0 = radius(X0) RV = radius(V) unitBall = BoxInfty(center = zero_vector(n), radius = 1, base_ring = base_ring) alpha_tau = (exp(tau*Ainfty) - 1 - tau*Ainfty)*(RX0 + RV/Ainfty) alpha_tau_B = (alpha_tau*np.identity(n)) * unitBall # compute the first element of the approximating sequence, Omega_0 #aux = expX0.Minkowski_sum(tau_V) #Omega0 = X0.convex_hull(aux.Minkowski_sum(alpha_tau_B)) beta_tau = (exp(tau*Ainfty) - 1 - tau*Ainfty)*(RV/Ainfty) beta_tau_B = (beta_tau*np.identity(n)) * unitBall #W_tau = tau_V.Minkowski_sum(beta_tau_B) # ################################################ # Build the sequence of approximations Omega_i # # ################################################ Omega_i_Family_SF = [_Omega_i_supports_inhom(d, X0) for d in dArray] # ################################################ # Build the approximating polyhedra # # ################################################ # each polytope is built using the support functions over-approximation Omega_i_Poly = list() # This loop can be vectorized (?) for i in range(N): # we have N polytopes # for each one, use all directions A = matrix(base_ring, k, n); b = vector(base_ring, k) for j in range(k): #run over directions s_fun = Omega_i_Family_SF[j][i] A.set_row(j, dList[j]) b[j] = s_fun Omega_i_Poly.append( polyhedron_from_Hrep(A, b, base_ring = base_ring) ) return Omega_i_Poly