diff --git a/src/cytools/__init__.py b/src/cytools/__init__.py index 038f4c3..4a47878 100644 --- a/src/cytools/__init__.py +++ b/src/cytools/__init__.py @@ -23,6 +23,7 @@ version = "1.2.4" versions_with_serious_bugs = [] + # Check for more recent versions of CYTools def check_for_updates(): """ @@ -52,10 +53,12 @@ def check_for_updates(): try: # get updated __init__ from github - p = requests.get("https://raw.githubusercontent.com/" - + "LiamMcAllisterGroup/cytools/main/cytools/" - + "__init__.py", - timeout=2) + p = requests.get( + "https://raw.githubusercontent.com/" + + "LiamMcAllisterGroup/cytools/main/cytools/" + + "__init__.py", + timeout=2, + ) # find/check the version in this file for l in p.text.split("\n"): @@ -63,34 +66,40 @@ def check_for_updates(): checked_version = True # parse version - latest_ver = tuple(int(c) for c in l.split("\"")[1].split(".")) + latest_ver = tuple(int(c) for c in l.split('"')[1].split(".")) ver = tuple(int(c) for c in version.split(".")) # check if latest_ver <= ver: - continue + continue # local version is old -> print warning - print("\nInfo: A more recent version of CYTools is available: " - f"v{ver[0]}.{ver[1]}.{ver[2]} -> " - f"v{latest_ver[0]}.{latest_ver[1]}.{latest_ver[2]}.\n" - "We recommend upgrading before continuing.\n" - "On Linux and macOS you can update CYTools by running " - "'cytools --update'\n" - "and on Windows you can do this by running the updater " - "tool.\n") + print( + "\nInfo: A more recent version of CYTools is available: " + f"v{ver[0]}.{ver[1]}.{ver[2]} -> " + f"v{latest_ver[0]}.{latest_ver[1]}.{latest_ver[2]}.\n" + "We recommend upgrading before continuing.\n" + "On Linux and macOS you can update CYTools by running " + "'cytools --update'\n" + "and on Windows you can do this by running the updater " + "tool.\n" + ) elif (not checked_bugs) and ("versions_with_serious_bugs =" in l): checked_bugs = True bad_versions = literal_eval(l.split("=")[1].strip()) if version in bad_versions: - print("\n****************************\n" - "Warning: This version of CYTools contains a serious" - " bug. Please upgrade to the latest version.\n" - "****************************\n") + print( + "\n****************************\n" + "Warning: This version of CYTools contains a serious" + " bug. Please upgrade to the latest version.\n" + "****************************\n" + ) if checked_version and checked_bugs: break except: - pass + pass + + check_for_updates() diff --git a/src/cytools/calabiyau.py b/src/cytools/calabiyau.py index 48f75f7..601e185 100644 --- a/src/cytools/calabiyau.py +++ b/src/cytools/calabiyau.py @@ -35,11 +35,17 @@ # CYTools imports from cytools import config from cytools.cone import Cone -from cytools.utils import (gcd_list, array_fmpz_to_int, - filter_tensor_indices, - symmetric_sparse_to_dense, - symmetric_dense_to_sparse, fmpq_to_float, - set_divisor_basis, set_curve_basis) +from cytools.utils import ( + gcd_list, + array_fmpz_to_int, + filter_tensor_indices, + symmetric_sparse_to_dense, + symmetric_dense_to_sparse, + fmpq_to_float, + set_divisor_basis, + set_curve_basis, +) + class CalabiYau: """ @@ -128,41 +134,70 @@ def __init__(self, toric_var, nef_partition=None): """ # We first make sure that the input triangulation is appropriate. # Regularity is not checked since it is generally slow. + triang = toric_var.triangulation() + if nef_partition is not None: if not config._exp_features_enabled: - raise Exception("The experimental features must be enabled to " - "construct CICYs.") + raise Exception( + "The experimental features must be enabled to " "construct CICYs." + ) # Verify that the input defines a nef-partition from cytools import Polytope + pts = toric_var.polytope().points() - convpoly = Polytope(pts[list(set.union(*[set(ii) for ii in nef_partition]))]) + convpoly = Polytope( + pts[list(set.union(*[set(ii) for ii in nef_partition]))] + ) if convpoly != toric_var.polytope(): raise ValueError("Input data does not define a nef partition") - polys = [Polytope(pts[[0]+list(ii)]) for ii in nef_partition] + polys = [Polytope(pts[[0] + list(ii)]) for ii in nef_partition] sumpoly = polys[0] - for i in range(1,len(polys)): + for i in range(1, len(polys)): sumpoly = sumpoly.minkowski_sum(polys[i]) if not sumpoly.is_reflexive(): raise ValueError("Input data does not define a nef partition") - triang = toric_var.triangulation() + triangpts = [tuple(pt) for pt in triang.points()] - parts = [tuple(triang.points_to_indices(pt) for pt in pp.points() if any(pt) and tuple(pt) in triangpts) - for pp in polys] + parts = [ + tuple( + triang.points_to_indices(pt) + for pt in pp.points() + if any(pt) and tuple(pt) in triangpts + ) + for pp in polys + ] self._nef_part = parts else: self._nef_part = None - if not toric_var.triangulation().is_fine(): + if not triang.is_fine(): raise ValueError("Triangulation is non-fine.") - if ((toric_var.dim() != 4 or not toric_var.triangulation().polytope().is_favorable(lattice="N")) - and not config._exp_features_enabled): - raise Exception("The experimental features must be enabled to " - "construct non-favorable CYs or CYs with " - "dimension other than 3.") - if not ((toric_var.triangulation().points().shape == toric_var.triangulation().polytope().points_not_interior_to_facets().shape - and all((toric_var.triangulation().points() == toric_var.triangulation().polytope().points_not_interior_to_facets()).flat)) - or (toric_var.triangulation().points().shape == toric_var.triangulation().polytope().points().shape - and all((toric_var.triangulation().points() == toric_var.triangulation().polytope().points()).flat))): - raise ValueError("Calabi-Yau hypersurfaces must be constructed either points not interior to facets or all points.") + if ( + toric_var.dim() != 4 or not triang.polytope().is_favorable(lattice="N") + ) and not config._exp_features_enabled: + raise Exception( + "The experimental features must be enabled to " + "construct non-favorable CYs or CYs with " + "dimension other than 3." + ) + if not ( + ( + triang.points().shape + == triang.polytope().points_not_interior_to_facets().shape + and all( + ( + triang.points() + == triang.polytope().points_not_interior_to_facets() + ).flat + ) + ) + or ( + triang.points().shape == triang.polytope().points().shape + and all((triang.points() == triang.polytope().points()).flat) + ) + ): + raise ValueError( + "Calabi-Yau hypersurfaces must be constructed either points not interior to facets or all points." + ) self._ambient_var = toric_var self._optimal_ambient_var = None self._is_hypersurface = nef_partition is None or len(nef_partition) == 1 @@ -177,7 +212,7 @@ def __init__(self, toric_var, nef_partition=None): self._divisor_basis_mat = None self._curve_basis = None self._curve_basis_mat = None - self._mori_cone = [None]*3 + self._mori_cone = [None] * 3 self._intersection_numbers = dict() self._prime_divs = None self._second_chern_class = None @@ -229,7 +264,7 @@ def clear_cache(self, recursive=False, only_in_basis=False): self._divisor_basis_mat = None self._curve_basis = None self._curve_basis_mat = None - self._mori_cone = [None]*3 + self._mori_cone = [None] * 3 self._intersection_numbers = dict() self._prime_divs = None self._second_chern_class = None @@ -265,38 +300,54 @@ def __repr__(self): d = self.dim() if self._is_hypersurface: if d == 2: - out_str = (f"A K3 hypersurface with h11={self.h11()} in a " - "3-dimensional toric variety") + out_str = ( + f"A K3 hypersurface with h11={self.h11()} in a " + "3-dimensional toric variety" + ) elif d == 3: - out_str = ("A Calabi-Yau 3-fold hypersurface with " - f"h11={self.h11()} and h21={self.h21()} in a " - "4-dimensional toric variety") + out_str = ( + "A Calabi-Yau 3-fold hypersurface with " + f"h11={self.h11()} and h21={self.h21()} in a " + "4-dimensional toric variety" + ) elif d == 4: - out_str = ("A Calabi-Yau 4-fold hypersurface with " - f"h11={self.h11()}, h12={self.h12()}, " - f"h13={self.h13()}, and h22={self.h22()} in a " - "5-dimensional toric variety") + out_str = ( + "A Calabi-Yau 4-fold hypersurface with " + f"h11={self.h11()}, h12={self.h12()}, " + f"h13={self.h13()}, and h22={self.h22()} in a " + "5-dimensional toric variety" + ) else: - out_str = (f"A Calabi-Yau {d}-fold hypersurface in a " - f"{d+1}-dimensional toric variety") + out_str = ( + f"A Calabi-Yau {d}-fold hypersurface in a " + f"{d+1}-dimensional toric variety" + ) else: dd = self.ambient_variety().dim() - if self._hodge_nums is None or d not in (2,3,4): - out_str = (f"A complete intersection Calabi-Yau {d}-fold in a " - f"{dd}-dimensional toric variety") + if self._hodge_nums is None or d not in (2, 3, 4): + out_str = ( + f"A complete intersection Calabi-Yau {d}-fold in a " + f"{dd}-dimensional toric variety" + ) elif d == 2: - out_str = (f"A complete intersection K3 surface with " - f"h11={self.h11()} in a " - f"{dd}-dimensional toric variety") + out_str = ( + f"A complete intersection K3 surface with " + f"h11={self.h11()} in a " + f"{dd}-dimensional toric variety" + ) elif d == 3: - out_str = (f"A complete intersection Calabi-Yau 3-fold with " - f"h11={self.h11()} h21={self.h21()} in a " - + f"{dd}-dimensional toric variety") + out_str = ( + f"A complete intersection Calabi-Yau 3-fold with " + f"h11={self.h11()} h21={self.h21()} in a " + + f"{dd}-dimensional toric variety" + ) elif d == 4: - out_str = (f"A complete intersection Calabi-Yau 4-fold with " - f"h11={self.h11()}, h12={self.h12()}, " - f"h13={self.h13()}, and h22={self.h22()} in a " - f"{dd}-dimensional toric variety") + out_str = ( + f"A complete intersection Calabi-Yau 4-fold with " + f"h11={self.h11()}, h12={self.h12()}, " + f"h13={self.h13()}, and h22={self.h22()} in a " + f"{dd}-dimensional toric variety" + ) return out_str def __eq__(self, other): @@ -336,11 +387,13 @@ def __eq__(self, other): """ if not isinstance(other, CalabiYau): return NotImplemented - is_triv_equiv = self.is_trivially_equivalent(other) + is_triv_equiv = self.is_trivially_equivalent(other) if is_triv_equiv: return True - warnings.warn("The comparison of CYs should not be done with ==. " - "Please use the is_trivially_equivalent function.") + warnings.warn( + "The comparison of CYs should not be done with ==. " + "Please use the is_trivially_equivalent function." + ) return False def __ne__(self, other): @@ -410,9 +463,9 @@ def __hash__(self): if self._is_hypersurface: self_orbit = self.triangulation().automorphism_orbit(on_faces_codim=2) self_orbit = tuple(tuple(tuple(s) for s in t) for t in self_orbit) - return hash((hash(self.triangulation().polytope()),hash(self_orbit))) + return hash((hash(self.triangulation().polytope()), hash(self_orbit))) else: - self._hash = hash((hash(self.ambient_variety()),hash(self._nef_part))) + self._hash = hash((hash(self.ambient_variety()), hash(self._nef_part))) return self._hash def is_trivially_equivalent(self, other): @@ -461,7 +514,9 @@ def is_trivially_equivalent(self, other): return False self_orbit = self.triangulation().automorphism_orbit(on_faces_codim=2) other_orbit = other.triangulation().automorphism_orbit(on_faces_codim=2) - return self_orbit.shape == other_orbit.shape and all((self_orbit == other_orbit).flat) + return self_orbit.shape == other_orbit.shape and all( + (self_orbit == other_orbit).flat + ) def ambient_variety(self): """ @@ -564,6 +619,7 @@ def ambient_dimension(self): ``` """ return self.ambient_variety().dim() + # aliases ambient_dim = ambient_dimension @@ -593,11 +649,16 @@ def dimension(self): """ if self._dim is not None: return self._dim + + tv = self.ambient_variety() + if self._is_hypersurface: - self._dim = self.ambient_variety().dim() - 1 + self._dim = tv.dim() - 1 else: - self._dim = self.ambient_variety().triangulation().dim() - len(self._nef_part) + self._dim = tv.triangulation().dim() - len(self._nef_part) + return self._dim + # aliases dim = dimension @@ -643,11 +704,12 @@ def hpq(self, p, q): if not self._is_hypersurface: if self._hodge_nums is None: self._compute_cicy_hodge_numbers() - return self._hodge_nums.get((p,q),0) - if self.dim() not in (2,3,4) and p!=1 and q!=1: - raise NotImplementedError("Only Calabi-Yaus of dimension 2-4 are " - "currently supported.") - return self.polytope().hpq(p,q,lattice="N") + return self._hodge_nums.get((p, q), 0) + if self.dim() not in (2, 3, 4) and p != 1 and q != 1: + raise NotImplementedError( + "Only Calabi-Yaus of dimension 2-4 are " "currently supported." + ) + return self.polytope().hpq(p, q, lattice="N") def h11(self): """ @@ -676,10 +738,11 @@ def h11(self): ``` """ if not self._is_hypersurface: - return self.hpq(1,1) - if self.dim() not in (2,3,4): - raise NotImplementedError("Only Calabi-Yaus of dimension 2-4 are " - "currently supported.") + return self.hpq(1, 1) + if self.dim() not in (2, 3, 4): + raise NotImplementedError( + "Only Calabi-Yaus of dimension 2-4 are " "currently supported." + ) return self.polytope().h11(lattice="N") def h12(self): @@ -712,11 +775,13 @@ def h12(self): ``` """ if not self._is_hypersurface: - return self.hpq(1,2) - if self.dim() not in (2,3,4): - raise NotImplementedError("Only Calabi-Yaus of dimension 2-4 are " - "currently supported.") + return self.hpq(1, 2) + if self.dim() not in (2, 3, 4): + raise NotImplementedError( + "Only Calabi-Yaus of dimension 2-4 are " "currently supported." + ) return self.polytope().h12(lattice="N") + # aliases h21 = h12 @@ -750,11 +815,13 @@ def h13(self): ``` """ if not self._is_hypersurface: - return self.hpq(1,3) - if self.dim() not in (2,3,4): - raise NotImplementedError("Only Calabi-Yaus of dimension 2-4 are " - "currently supported.") + return self.hpq(1, 3) + if self.dim() not in (2, 3, 4): + raise NotImplementedError( + "Only Calabi-Yaus of dimension 2-4 are " "currently supported." + ) return self.polytope().h13(lattice="N") + # aliases h31 = h13 @@ -785,10 +852,11 @@ def h22(self): ``` """ if not self._is_hypersurface: - return self.hpq(2,2) - if self.dim() not in (2,3,4): - raise NotImplementedError("Only Calabi-Yaus of dimension 2-4 are " - "currently supported.") + return self.hpq(2, 2) + if self.dim() not in (2, 3, 4): + raise NotImplementedError( + "Only Calabi-Yaus of dimension 2-4 are " "currently supported." + ) return self.polytope().h22(lattice="N") def chi(self): @@ -824,20 +892,21 @@ def chi(self): if "chi" in self._hodge_nums: return self._hodge_nums["chi"] chi = 0 - for i in range(2*self.dim()+1): - ii = min(i,self.dim()) + for i in range(2 * self.dim() + 1): + ii = min(i, self.dim()) jj = i - ii while True: - chi += (-1 if i%2 else 1)*self._hodge_nums[(ii,jj)] + chi += (-1 if i % 2 else 1) * self._hodge_nums[(ii, jj)] ii -= 1 jj += 1 if ii < 0 or jj > self.dim(): break self._hodge_nums["chi"] = chi return self._hodge_nums["chi"] - if self.dim() not in (2,3,4): - raise NotImplementedError("Only Calabi-Yaus of dimension 2-4 are " - "currently supported.") + if self.dim() not in (2, 3, 4): + raise NotImplementedError( + "Only Calabi-Yaus of dimension 2-4 are " "currently supported." + ) return self.polytope().chi(lattice="N") def glsm_charge_matrix(self, include_origin=True): @@ -866,12 +935,12 @@ def glsm_charge_matrix(self, include_origin=True): ``` """ if self._glsm_charge_matrix is not None: - return np.array(self._glsm_charge_matrix)[:,(0 if include_origin else 1):] - pts = [0]+list(self.prime_toric_divisors()) + return np.array(self._glsm_charge_matrix)[:, (0 if include_origin else 1) :] + pts = [0] + list(self.prime_toric_divisors()) self._glsm_charge_matrix = self.polytope().glsm_charge_matrix( - include_origin=True, - points=pts) - return np.array(self._glsm_charge_matrix)[:,(0 if include_origin else 1):] + include_origin=True, points=pts + ) + return np.array(self._glsm_charge_matrix)[:, (0 if include_origin else 1) :] def glsm_linear_relations(self, include_origin=True): """ @@ -903,12 +972,16 @@ def glsm_linear_relations(self, include_origin=True): ``` """ if self._glsm_linrels is not None: - return np.array(self._glsm_linrels)[(0 if include_origin else 1):,(0 if include_origin else 1):] - pts = [0]+list(self.prime_toric_divisors()) + return np.array(self._glsm_linrels)[ + (0 if include_origin else 1) :, (0 if include_origin else 1) : + ] + pts = [0] + list(self.prime_toric_divisors()) self._glsm_linrels = self.polytope().glsm_linear_relations( - include_origin=True, - points=pts) - return np.array(self._glsm_linrels)[(0 if include_origin else 1):,(0 if include_origin else 1):] + include_origin=True, points=pts + ) + return np.array(self._glsm_linrels)[ + (0 if include_origin else 1) :, (0 if include_origin else 1) : + ] def divisor_basis(self, include_origin=True, as_matrix=False): """ @@ -950,19 +1023,24 @@ def divisor_basis(self, include_origin=True, as_matrix=False): ``` """ if self._divisor_basis is None: - pts = [0]+list(self.prime_toric_divisors()) - self.set_divisor_basis(self.polytope().glsm_basis( - integral=True, - include_origin=True, - points=pts)) + pts = [0] + list(self.prime_toric_divisors()) + self.set_divisor_basis( + self.polytope().glsm_basis( + integral=True, include_origin=True, points=pts + ) + ) if len(self._divisor_basis.shape) == 1: if 0 in self._divisor_basis and not include_origin: - raise ValueError("The basis was requested not including the " - "origin, but it is included in the current basis.") + raise ValueError( + "The basis was requested not including the " + "origin, but it is included in the current basis." + ) if as_matrix: - return np.array(self._divisor_basis_mat[:,(0 if include_origin else 1):]) + return np.array( + self._divisor_basis_mat[:, (0 if include_origin else 1) :] + ) return np.array(self._divisor_basis) - (0 if include_origin else 1) - return np.array(self._divisor_basis[:,(0 if include_origin else 1):]) + return np.array(self._divisor_basis[:, (0 if include_origin else 1) :]) def set_divisor_basis(self, basis, include_origin=True): """ @@ -1052,19 +1130,27 @@ def curve_basis(self, include_origin=True, as_matrix=False): ``` """ if self._curve_basis is None: - self.set_divisor_basis(self.polytope().glsm_basis( - integral=True, - include_origin=True, - points=self.polytope().points_to_indices(self.triangulation().points())) - ) + self.set_divisor_basis( + self.polytope().glsm_basis( + integral=True, + include_origin=True, + points=self.polytope().points_to_indices( + self.triangulation().points() + ), + ) + ) if len(self._curve_basis.shape) == 1: if 0 in self._curve_basis and not include_origin: - raise ValueError("The basis was requested not including the " - "origin, but it is included in the current basis.") + raise ValueError( + "The basis was requested not including the " + "origin, but it is included in the current basis." + ) if as_matrix: - return np.array(self._curve_basis_mat[:,(0 if include_origin else 1):]) + return np.array( + self._curve_basis_mat[:, (0 if include_origin else 1) :] + ) return np.array(self._curve_basis) - (0 if include_origin else 1) - return np.array(self._curve_basis[:,(0 if include_origin else 1):]) + return np.array(self._curve_basis[:, (0 if include_origin else 1) :]) def set_curve_basis(self, basis, include_origin=True): """ @@ -1121,12 +1207,19 @@ def set_curve_basis(self, basis, include_origin=True): # shared with the ToricVariety class. set_curve_basis(self, basis, include_origin=include_origin) - def intersection_numbers(self, in_basis=False, format="dok", - zero_as_anticanonical=False, backend="all", - check=True, backend_error_tol=1e-3, - round_to_zero_threshold=1e-3, - round_to_integer_error_tol=5e-2, verbose=0, - exact_arithmetic=False): + def intersection_numbers( + self, + in_basis=False, + format="dok", + zero_as_anticanonical=False, + backend="all", + check=True, + backend_error_tol=1e-3, + round_to_zero_threshold=1e-3, + round_to_integer_error_tol=5e-2, + verbose=0, + exact_arithmetic=False, + ): """ **Description:** Returns the intersection numbers of the Calabi-Yau manifold. @@ -1248,31 +1341,53 @@ def intersection_numbers(self, in_basis=False, format="dok", return copy.copy(self._intersection_numbers[args_id]) # do the calculation - if ((False,False,False,"dok") not in self._intersection_numbers - or ((False,False,True,"dok") not in self._intersection_numbers - and exact_arithmetic)): + if (False, False, False, "dok") not in self._intersection_numbers or ( + (False, False, True, "dok") not in self._intersection_numbers + and exact_arithmetic + ): ambient_intnums = self.ambient_variety().intersection_numbers( - in_basis=False, format="dok", backend=backend, - check=check, backend_error_tol=backend_error_tol, - round_to_zero_threshold=round_to_zero_threshold, - round_to_integer_error_tol=round_to_integer_error_tol, - verbose=verbose, exact_arithmetic=exact_arithmetic) + in_basis=False, + format="dok", + backend=backend, + check=check, + backend_error_tol=backend_error_tol, + round_to_zero_threshold=round_to_zero_threshold, + round_to_integer_error_tol=round_to_integer_error_tol, + verbose=verbose, + exact_arithmetic=exact_arithmetic, + ) if self._is_hypersurface: - intnums_cy = {ii[1:]:-ambient_intnums[ii] for ii in ambient_intnums if 0 in ii} + intnums_cy = { + ii[1:]: -ambient_intnums[ii] for ii in ambient_intnums if 0 in ii + } else: - triang_pts = [tuple(pt) for pt in self.ambient_variety().triangulation().points()] + triang_pts = [ + tuple(pt) for pt in self.ambient_variety().triangulation().points() + ] parts = self._nef_part ambient_dim = self.ambient_dim() intnums_dict = ambient_intnums for dd in range(len(parts)): intnums_dict_tmp = defaultdict(lambda: 0) for ii in intnums_dict: - choices = set(tuple(sorted(c for i,c in enumerate(ii) if i!=j)) for j in range(ambient_dim-dd) if ii[j] in parts[dd]) + choices = set( + tuple(sorted(c for i, c in enumerate(ii) if i != j)) + for j in range(ambient_dim - dd) + if ii[j] in parts[dd] + ) for c in choices: intnums_dict_tmp[c] += intnums_dict[ii] - intnums_dict = {ii:intnums_dict_tmp[ii] for ii in intnums_dict_tmp if abs(intnums_dict_tmp[ii]) > round_to_zero_threshold} + intnums_dict = { + ii: intnums_dict_tmp[ii] + for ii in intnums_dict_tmp + if abs(intnums_dict_tmp[ii]) > round_to_zero_threshold + } intnums_cy = intnums_dict - if all(abs(round(intnums_cy[ii])-intnums_cy[ii]) < round_to_integer_error_tol for ii in intnums_cy): + if all( + abs(round(intnums_cy[ii]) - intnums_cy[ii]) + < round_to_integer_error_tol + for ii in intnums_cy + ): self._is_smooth = True for ii in intnums_cy: intnums_cy[ii] = int(round(intnums_cy[ii])) @@ -1281,48 +1396,101 @@ def intersection_numbers(self, in_basis=False, format="dok", # Now we find the prime toric divisors and reindex accordingly intnum_ind = set.union(*[set(ii) for ii in intnums_cy]) triang_inds = sorted(intnum_ind) - self._prime_divs = tuple(self.triangulation().triangulation_to_polytope_indices([i for i in triang_inds if i])) - divs_dict = {ii:i for i,ii in enumerate(self._prime_divs,1)} + self._prime_divs = tuple( + self.triangulation().triangulation_to_polytope_indices( + [i for i in triang_inds if i] + ) + ) + divs_dict = {ii: i for i, ii in enumerate(self._prime_divs, 1)} divs_dict[0] = 0 - intnums_cy = {tuple(divs_dict[i] for i in ii):intnums_cy[ii] for ii in intnums_cy} + intnums_cy = { + tuple(divs_dict[i] for i in ii): intnums_cy[ii] for ii in intnums_cy + } # If there are some non-intersecting divisors we construct a better # toric variety in the background - if len(self._prime_divs) == self.ambient_variety().triangulation().points().shape[0]-1: + if ( + len(self._prime_divs) + == self.ambient_variety().triangulation().points().shape[0] - 1 + ): self._optimal_ambient_var = self._ambient_var else: heights = self.triangulation().heights()[triang_inds] try: - self._optimal_ambient_var = self.polytope().triangulate(heights=heights, points=self.polytope().points_to_indices( - self.polytope().points()[[0]+list(self._prime_divs)])).get_toric_variety() + self._optimal_ambient_var = ( + self.polytope() + .triangulate( + heights=heights, + points=self.polytope().points_to_indices( + self.polytope().points()[ + [0] + list(self._prime_divs) + ] + ), + ) + .get_toric_variety() + ) except: - raise NotImplementedError("This type of complete intersection is not supported.") - self._intersection_numbers[(False,False,exact_arithmetic,"dok")] = intnums_cy + raise NotImplementedError( + "This type of complete intersection is not supported." + ) + self._intersection_numbers[(False, False, exact_arithmetic, "dok")] = ( + intnums_cy + ) # Now intersection numbers have been computed # We now compute the intersection numbers of the basis if necessary if zero_as_anticanonical and not in_basis: - self._intersection_numbers[(True,False,exact_arithmetic,"dok")] = self._intersection_numbers[(False,False,exact_arithmetic,"dok")] - for ii in self._intersection_numbers[(True,False,exact_arithmetic,"dok")]: + self._intersection_numbers[(True, False, exact_arithmetic, "dok")] = ( + self._intersection_numbers[(False, False, exact_arithmetic, "dok")] + ) + for ii in self._intersection_numbers[ + (True, False, exact_arithmetic, "dok") + ]: if 0 not in ii: continue - self._intersection_numbers[(True,False,exact_arithmetic,"dok")][ii] *= (-1 if sum(jj == 0 for jj in ii)%2 == 1 else 1) + self._intersection_numbers[(True, False, exact_arithmetic, "dok")][ + ii + ] *= (-1 if sum(jj == 0 for jj in ii) % 2 == 1 else 1) elif in_basis: basis = self.divisor_basis() - if len(basis.shape) == 2: # If basis is matrix - self._intersection_numbers[(False,True,exact_arithmetic,"dense")] = ( - symmetric_sparse_to_dense(self._intersection_numbers[(False,False,exact_arithmetic,"dok")], basis)) - self._intersection_numbers[(False,True,exact_arithmetic,"dok")] = ( - symmetric_dense_to_sparse(self._intersection_numbers[(False,True,exact_arithmetic,"dense")])) + if len(basis.shape) == 2: # If basis is matrix + self._intersection_numbers[(False, True, exact_arithmetic, "dense")] = ( + symmetric_sparse_to_dense( + self._intersection_numbers[ + (False, False, exact_arithmetic, "dok") + ], + basis, + ) + ) + self._intersection_numbers[(False, True, exact_arithmetic, "dok")] = ( + symmetric_dense_to_sparse( + self._intersection_numbers[ + (False, True, exact_arithmetic, "dense") + ] + ) + ) else: - self._intersection_numbers[(False,True,exact_arithmetic,"dok")] = filter_tensor_indices( - self._intersection_numbers[(False,False,exact_arithmetic,"dok")], basis) + self._intersection_numbers[(False, True, exact_arithmetic, "dok")] = ( + filter_tensor_indices( + self._intersection_numbers[ + (False, False, exact_arithmetic, "dok") + ], + basis, + ) + ) # Intersection numbers of the basis are now done # Finally, we convert into the desired format if format == "coo": - tmpintnums = self._intersection_numbers[(zero_as_anticanonical,in_basis,exact_arithmetic,"dok")] - self._intersection_numbers[args_id] = np.array([list(ii)+[tmpintnums[ii]] for ii in tmpintnums]) + tmpintnums = self._intersection_numbers[ + (zero_as_anticanonical, in_basis, exact_arithmetic, "dok") + ] + self._intersection_numbers[args_id] = np.array( + [list(ii) + [tmpintnums[ii]] for ii in tmpintnums] + ) elif format == "dense": - self._intersection_numbers[args_id] = ( - symmetric_sparse_to_dense(self._intersection_numbers[(zero_as_anticanonical,in_basis,exact_arithmetic,"dok")])) + self._intersection_numbers[args_id] = symmetric_sparse_to_dense( + self._intersection_numbers[ + (zero_as_anticanonical, in_basis, exact_arithmetic, "dok") + ] + ) return copy.copy(self._intersection_numbers[args_id]) def prime_toric_divisors(self): @@ -1356,7 +1524,9 @@ def prime_toric_divisors(self): if self._prime_divs is not None: return self._prime_divs if self._is_hypersurface: - self._prime_divs = tuple(range(1,self.polytope().points_not_interior_to_facets().shape[0])) + self._prime_divs = tuple( + range(1, self.polytope().points_not_interior_to_facets().shape[0]) + ) self._optimal_ambient_var = self._ambient_var else: # For CICYs we have to compute intersection numbers and the @@ -1397,7 +1567,7 @@ def second_chern_class(self, in_basis=False, include_origin=True): if self.dim() != 3: raise NotImplementedError("This function currently only supports 3-folds.") if self._second_chern_class is None: - c2 = np.zeros(len(self.prime_toric_divisors())+1, dtype=int) + c2 = np.zeros(len(self.prime_toric_divisors()) + 1, dtype=int) intnums = self.intersection_numbers(in_basis=False) for ii in intnums: if ii[0] == 0: @@ -1418,10 +1588,10 @@ def second_chern_class(self, in_basis=False, include_origin=True): self._second_chern_class = c2 if in_basis: basis = self.divisor_basis() - if len(basis.shape) == 2: # If basis is matrix + if len(basis.shape) == 2: # If basis is matrix return self._second_chern_class.dot(basis.T) return np.array(self._second_chern_class[basis]) - return np.array(self._second_chern_class)[(0 if include_origin else 1):] + return np.array(self._second_chern_class)[(0 if include_origin else 1) :] def is_smooth(self): """ @@ -1449,7 +1619,7 @@ def is_smooth(self): if self._is_hypersurface: self._is_smooth = self.ambient_variety().canonical_divisor_is_smooth() else: - self.intersection_numbers() # The variable is set while computing intersection numbers + self.intersection_numbers() # The variable is set while computing intersection numbers return self._is_smooth def toric_mori_cone(self, in_basis=False, include_origin=True): @@ -1484,11 +1654,13 @@ def toric_mori_cone(self, in_basis=False, include_origin=True): ``` """ if self._mori_cone[0] is None: - if self._optimal_ambient_var is None: # Make sure self._optimal_ambient_var is set + if ( + self._optimal_ambient_var is None + ): # Make sure self._optimal_ambient_var is set self.prime_toric_divisors() self._mori_cone[0] = self._optimal_ambient_var.mori_cone() # 0: All divs, 1: No origin, 2: In basis - args_id = ((not include_origin)*1 if not in_basis else 0) + in_basis*2 + args_id = ((not include_origin) * 1 if not in_basis else 0) + in_basis * 2 if self._mori_cone[args_id] is not None: return self._mori_cone[args_id] rays = self._mori_cone[0].rays() @@ -1496,13 +1668,13 @@ def toric_mori_cone(self, in_basis=False, include_origin=True): if include_origin and not in_basis: new_rays = rays elif not include_origin and not in_basis: - new_rays = rays[:,1:] + new_rays = rays[:, 1:] else: - if len(basis.shape) == 2: # If basis is matrix + if len(basis.shape) == 2: # If basis is matrix new_rays = rays.dot(basis.T) else: - new_rays = rays[:,basis] - c = Cone(new_rays, check=len(basis.shape)==2) + new_rays = rays[:, basis] + c = Cone(new_rays, check=len(basis.shape) == 2) self._mori_cone[args_id] = c return self._mori_cone[args_id] @@ -1555,7 +1727,7 @@ def toric_effective_cone(self): """ if self._eff_cone is not None: return self._eff_cone - self._eff_cone = Cone(self.curve_basis(include_origin=False,as_matrix=True).T) + self._eff_cone = Cone(self.curve_basis(include_origin=False, as_matrix=True).T) return self._eff_cone def compute_cy_volume(self, tloc): @@ -1582,20 +1754,18 @@ def compute_cy_volume(self, tloc): # 3.4999999988856496 ``` """ - intnums = self.intersection_numbers(in_basis=True, - exact_arithmetic=False) + intnums = self.intersection_numbers(in_basis=True, exact_arithmetic=False) xvol = 0 basis = self.divisor_basis() - if len(basis.shape) == 2: # If basis is matrix + if len(basis.shape) == 2: # If basis is matrix tmp = np.array(intnums) for i in range(self.dim()): - tmp = np.tensordot(tmp, tloc, axes=[[self.dim()-1-i],[0]]) - xvol = tmp/factorial(self.dim()) + tmp = np.tensordot(tmp, tloc, axes=[[self.dim() - 1 - i], [0]]) + xvol = tmp / factorial(self.dim()) else: for ii in intnums: - mult = np.prod([factorial(c) - for c in Counter(ii).values()]) - xvol += intnums[ii]*np.prod([tloc[int(j)] for j in ii])/mult + mult = np.prod([factorial(c) for c in Counter(ii).values()]) + xvol += intnums[ii] * np.prod([tloc[int(j)] for j in ii]) / mult return xvol def compute_divisor_volumes(self, tloc, in_basis=False): @@ -1631,7 +1801,9 @@ def compute_divisor_volumes(self, tloc, in_basis=False): ``` """ if not in_basis: - tloc_new = np.array(tloc).dot(self.divisor_basis(as_matrix=True, include_origin=False)) + tloc_new = np.array(tloc).dot( + self.divisor_basis(as_matrix=True, include_origin=False) + ) intnums = self.intersection_numbers(in_basis=False, exact_arithmetic=False) tau = np.zeros(len(self.prime_toric_divisors()), dtype=float) for ii in intnums: @@ -1639,25 +1811,32 @@ def compute_divisor_volumes(self, tloc, in_basis=False): continue c = Counter(ii) for j in c.keys(): - tau[j-1] += intnums[ii] * np.prod( - [tloc_new[k-1]**(c[k]-(j==k))/factorial(c[k]-(j==k)) - for k in c.keys()]) + tau[j - 1] += intnums[ii] * np.prod( + [ + tloc_new[k - 1] ** (c[k] - (j == k)) + / factorial(c[k] - (j == k)) + for k in c.keys() + ] + ) return np.array(tau) intnums = self.intersection_numbers(in_basis=True, exact_arithmetic=False) basis = self.divisor_basis() - if len(basis.shape) == 2: # If basis is matrix + if len(basis.shape) == 2: # If basis is matrix tmp = np.array(intnums) - for i in range(1,self.dim()): - tmp = np.tensordot(tmp, tloc, axes=[[self.dim()-1-i],[0]]) - tau = tmp/factorial(self.dim()-1) + for i in range(1, self.dim()): + tmp = np.tensordot(tmp, tloc, axes=[[self.dim() - 1 - i], [0]]) + tau = tmp / factorial(self.dim() - 1) else: tau = np.zeros(len(basis), dtype=float) for ii in intnums: c = Counter(ii) for j in c.keys(): tau[j] += intnums[ii] * np.prod( - [tloc[k]**(c[k]-(j==k))/factorial(c[k]-(j==k)) - for k in c.keys()]) + [ + tloc[k] ** (c[k] - (j == k)) / factorial(c[k] - (j == k)) + for k in c.keys() + ] + ) return np.array(tau) def compute_curve_volumes(self, tloc, only_extremal=False): @@ -1734,28 +1913,29 @@ def compute_kappa_matrix(self, tloc): raise NotImplementedError("This function only supports Calabi-Yau 3-folds.") intnums = self.intersection_numbers(in_basis=True, exact_arithmetic=False) basis = self.divisor_basis() - if len(basis.shape) == 2: # If basis is matrix - AA = np.tensordot(intnums, tloc, axes=[[2],[0]]) + if len(basis.shape) == 2: # If basis is matrix + AA = np.tensordot(intnums, tloc, axes=[[2], [0]]) return AA - AA = np.zeros((len(basis),)*2, dtype=float) + AA = np.zeros((len(basis),) * 2, dtype=float) for ii in intnums: ii_list = Counter(ii).most_common(3) - if len(ii_list)==1: - AA[ii_list[0][0],ii_list[0][0]] += intnums[ii]*tloc[ii_list[0][0]] - elif len(ii_list)==2: - AA[ii_list[0][0],ii_list[0][0]] += intnums[ii]*tloc[ii_list[1][0]] - AA[ii_list[0][0],ii_list[1][0]] += intnums[ii]*tloc[ii_list[0][0]] - AA[ii_list[1][0],ii_list[0][0]] += intnums[ii]*tloc[ii_list[0][0]] - elif len(ii_list)==3: - AA[ii_list[0][0],ii_list[1][0]] += intnums[ii]*tloc[ii_list[2][0]] - AA[ii_list[1][0],ii_list[0][0]] += intnums[ii]*tloc[ii_list[2][0]] - AA[ii_list[0][0],ii_list[2][0]] += intnums[ii]*tloc[ii_list[1][0]] - AA[ii_list[2][0],ii_list[0][0]] += intnums[ii]*tloc[ii_list[1][0]] - AA[ii_list[1][0],ii_list[2][0]] += intnums[ii]*tloc[ii_list[0][0]] - AA[ii_list[2][0],ii_list[1][0]] += intnums[ii]*tloc[ii_list[0][0]] + if len(ii_list) == 1: + AA[ii_list[0][0], ii_list[0][0]] += intnums[ii] * tloc[ii_list[0][0]] + elif len(ii_list) == 2: + AA[ii_list[0][0], ii_list[0][0]] += intnums[ii] * tloc[ii_list[1][0]] + AA[ii_list[0][0], ii_list[1][0]] += intnums[ii] * tloc[ii_list[0][0]] + AA[ii_list[1][0], ii_list[0][0]] += intnums[ii] * tloc[ii_list[0][0]] + elif len(ii_list) == 3: + AA[ii_list[0][0], ii_list[1][0]] += intnums[ii] * tloc[ii_list[2][0]] + AA[ii_list[1][0], ii_list[0][0]] += intnums[ii] * tloc[ii_list[2][0]] + AA[ii_list[0][0], ii_list[2][0]] += intnums[ii] * tloc[ii_list[1][0]] + AA[ii_list[2][0], ii_list[0][0]] += intnums[ii] * tloc[ii_list[1][0]] + AA[ii_list[1][0], ii_list[2][0]] += intnums[ii] * tloc[ii_list[0][0]] + AA[ii_list[2][0], ii_list[1][0]] += intnums[ii] * tloc[ii_list[0][0]] else: raise Exception("Error: Inconsistent intersection numbers.") return AA + # aliases compute_AA = compute_kappa_matrix @@ -1826,7 +2006,7 @@ def compute_inverse_kahler_metric(self, tloc): xvol = self.compute_cy_volume(tloc) Tau = self.compute_divisor_volumes(tloc, in_basis=True) AA = self.compute_AA(tloc) - Kinv = 4*(np.outer(Tau,Tau) - AA*xvol) + Kinv = 4 * (np.outer(Tau, Tau) - AA * xvol) return Kinv def compute_kahler_metric(self, tloc): @@ -1897,24 +2077,33 @@ def _compute_cicy_hodge_numbers(self, only_from_cache=False): ``` """ if self._is_hypersurface: - raise NotImplementedError("This function should only be used for codim > 2 CICYs.") + raise NotImplementedError( + "This function should only be used for codim > 2 CICYs." + ) if self._hodge_nums is not None: - return + return codim = self.ambient_variety().dim() - self.dim() poly = self.ambient_variety().polytope() vert_ind = poly.points_to_indices(poly.vertices()) - nef_part_fs = frozenset(frozenset(i for i in part if i in vert_ind) for part in self._nef_part) + nef_part_fs = frozenset( + frozenset(i for i in part if i in vert_ind) for part in self._nef_part + ) matched_hodge_nums = () + def search_in_cache(): for args_id in poly._nef_parts: # Search only the ones with correct codim, computed hodge numbers, and without products or projections if args_id[-2] != codim or not args_id[-1] or args_id[1] or args_id[2]: continue - for i,nef_part in enumerate(poly._nef_parts[args_id][0]): - tmp_fs = frozenset(frozenset(ii for ii in part if ii in vert_ind) for part in nef_part) + for i, nef_part in enumerate(poly._nef_parts[args_id][0]): + tmp_fs = frozenset( + frozenset(ii for ii in part if ii in vert_ind) + for part in nef_part + ) if tmp_fs == nef_part_fs: return poly._nef_parts[args_id][1][i] return () + matched_hodge_nums = search_in_cache() if not len(matched_hodge_nums) and not only_from_cache: poly.nef_partitions() @@ -1923,15 +2112,17 @@ def search_in_cache(): poly.nef_partitions(keep_symmetric=True) matched_hodge_nums = search_in_cache() if not len(matched_hodge_nums) and not only_from_cache: - raise NotImplementedError("This type of complete intersection is not supported.") + raise NotImplementedError( + "This type of complete intersection is not supported." + ) if len(matched_hodge_nums): self._hodge_nums = dict() n = 0 - for i in range(2*self.dim()+1): - ii = min(i,self.dim()) + for i in range(2 * self.dim() + 1): + ii = min(i, self.dim()) jj = i - ii while True: - self._hodge_nums[(ii,jj)] = matched_hodge_nums[n] + self._hodge_nums[(ii, jj)] = matched_hodge_nums[n] n += 1 ii -= 1 jj += 1 @@ -1940,13 +2131,15 @@ def search_in_cache(): # GVs # --- - def _compute_gvs_gws(self, - gv_or_gw: str, - grading_vec: "ArrayLike"=None, - max_deg: bool=None, - min_points: bool=None, - basis: "ArrayLike"=None, - format: str=None): + def _compute_gvs_gws( + self, + gv_or_gw: str, + grading_vec: "ArrayLike" = None, + max_deg: bool = None, + min_points: bool = None, + basis: "ArrayLike" = None, + format: str = None, + ): """ **Description:** Wrapper for cygv GV and GW computations. A method of cytools.CalabiYau @@ -1973,7 +2166,7 @@ def _compute_gvs_gws(self, raise ValueError("Either max_deg or min_points must be set!") # get basics - kappa = self.intersection_numbers(in_basis=True, format='coo') + kappa = self.intersection_numbers(in_basis=True, format="coo") glsm = self.curve_basis(include_origin=False, as_matrix=True) mori = self.toric_mori_cone(in_basis=True) generators = mori.rays() @@ -1983,42 +2176,48 @@ def _compute_gvs_gws(self, grading_vec = mori.find_grading_vector() # compute the GVs - if gv_or_gw=='gv': + if gv_or_gw == "gv": fct = cygv.compute_gv else: fct = cygv.compute_gw - invariants = fct(generators = mori.rays(), - grading_vector = grading_vec, - q = self.curve_basis(include_origin=False, as_matrix=True), - intnums = self.intersection_numbers(in_basis=True, format='dok'), - max_deg = max_deg, - min_points = min_points) - + invariants = fct( + generators=mori.rays(), + grading_vector=grading_vec, + q=self.curve_basis(include_origin=False, as_matrix=True), + intnums=self.intersection_numbers(in_basis=True, format="dok"), + max_deg=max_deg, + min_points=min_points, + ) + # format/return the GVs - if format=='coo': - return [list(r[0])+[r[1]] for r in invariants] - + if format == "coo": + return [list(r[0]) + [r[1]] for r in invariants] + # convert to dok for subsequent processing invariants = dict(invariants) - if format=='dok': + if format == "dok": return invariants # return in Invariant class - invariants = Invariants(gv_or_gw, - invariants, - grading_vec, - max_deg, - calabiyau=self, - basis=basis) + invariants = Invariants( + gv_or_gw, + invariants, + grading_vec, + max_deg, + calabiyau=self, + basis=basis, + ) return invariants - def compute_gvs(self, - grading_vec: "ArrayLike"=None, - max_deg: bool=None, - min_points: bool=None, - format: str=None): + def compute_gvs( + self, + grading_vec: "ArrayLike" = None, + max_deg: bool = None, + min_points: bool = None, + format: str = None, + ): """ **Description:** Wrapper for cygv GV computations. A method of cytools.CalabiYau @@ -2035,14 +2234,17 @@ def compute_gvs(self, **Returns:** The GV invariants. """ - return self._compute_gvs_gws('gv', grading_vec, max_deg, min_points, format) + return self._compute_gvs_gws("gv", grading_vec, max_deg, min_points, format) + compute_gv = compute_gvs - def compute_gws(self, - grading_vec=None, - max_deg=None, - min_points=None, - format: str=None): + def compute_gws( + self, + grading_vec=None, + max_deg=None, + min_points=None, + format: str = None, + ): """ **Description:** Wrapper for cygv GW computations. A method of cytools.CalabiYau @@ -2059,20 +2261,25 @@ def compute_gws(self, **Returns:** The GW invariants. """ - return self._compute_gvs_gws('gw', grading_vec, max_deg, min_points, format) + return self._compute_gvs_gws("gw", grading_vec, max_deg, min_points, format) + compute_gw = compute_gws -class Invariants(): + +class Invariants: """ This class contains GV or GW-information. """ - def __init__(self, - invariant_type: str, - charge2invariant, - grading_vec: "ArrayLike"=None, - cutoff: int=None, - calabiyau: "CalabiYau"=None, - basis: "ArrayLike"=None): + + def __init__( + self, + invariant_type: str, + charge2invariant, + grading_vec: "ArrayLike" = None, + cutoff: int = None, + calabiyau: "CalabiYau" = None, + basis: "ArrayLike" = None, + ): """ **Description:** Container for GV or GW invariant information @@ -2086,15 +2293,15 @@ def __init__(self, **Returns:** The GW invariants. """ - if invariant_type not in ['gv','gw']: + if invariant_type not in ["gv", "gw"]: raise ValueError(f"Invariant type '{invariant_type}' not recognized") self._type = invariant_type - if isinstance(charge2invariant,dict): + if isinstance(charge2invariant, dict): self._charge2invariant = charge2invariant else: # assume charge2invariant is of coo format - self._charge2invariant = {tuple(r[:-1]):r[-1] for r in charge2invariant} + self._charge2invariant = {tuple(r[:-1]): r[-1] for r in charge2invariant} self._grading_vec = grading_vec self._cutoff = cutoff @@ -2104,8 +2311,10 @@ def __init__(self, if basis is not None: charges = self._charge2invariant.keys() invariants = self._charge2invariant.values() - charges = np.array(list(charges))@basis.T - self._charge2invariant = {tuple(r):_gv for r, _gv in zip(charges, invariants)} + charges = np.array(list(charges)) @ basis.T + self._charge2invariant = { + tuple(r): _gv for r, _gv in zip(charges, invariants) + } # standard methods # ---------------- @@ -2128,16 +2337,16 @@ def __str__(self): # cutoff out_str += " and cutoff=" - + if self._cutoff is not None: out_str += str(self._cutoff) else: out_str += "?" - return(out_str) - + return out_str + def __repr__(self): - return(str(self)) + return str(self) # getters # ------- @@ -2194,10 +2403,14 @@ def charges(self, by_deg=False, as_np_arr=False): # group by degree if self._grading_vec is None: - raise ValueError("Can't group charges by degree if no grading vector is known") - return _group_by_deg(self._charge2invariant.keys(), - self._grading_vec, - as_np_arr=as_np_arr) + raise ValueError( + "Can't group charges by degree if no grading vector is known" + ) + return _group_by_deg( + self._charge2invariant.keys(), + self._grading_vec, + as_np_arr=as_np_arr, + ) def cone(self): """ @@ -2214,7 +2427,7 @@ def cone(self): """ return Cone(self.charges(as_np_arr=True)) - @property + @property def gvs(self) -> set: """ **Description:** @@ -2226,10 +2439,11 @@ def gvs(self) -> set: **Returns:** The GV invariants. """ - if self._type=='gv': + if self._type == "gv": return set(self._charge2invariant.values()) else: return None + @property def gws(self) -> set: """ @@ -2242,7 +2456,7 @@ def gws(self) -> set: **Returns:** he GW invariants. """ - if self._type=='gw': + if self._type == "gw": return set(self._charge2invariant.values()) else: return None @@ -2261,21 +2475,22 @@ def invariant(self, charge, check_deg=True): **Returns:** *(int)* The GV invariant. """ - out = self._charge2invariant.get(tuple(charge),None) + out = self._charge2invariant.get(tuple(charge), None) if check_deg and (out is None): - if np.dot(charge,self._grading_vec)<=self._cutoff: - out = 0 # deg<=cutoff but not in dict -> 0 GV + if np.dot(charge, self._grading_vec) <= self._cutoff: + out = 0 # deg<=cutoff but not in dict -> 0 GV return out def gv(self, charge, check_deg=True): - if self._type=='gv': + if self._type == "gv": return self.invariant(charge, check_deg) else: return None + def gw(self, charge, check_deg=True): - if self._type=='gw': + if self._type == "gw": return self.invariant(charge, check_deg) else: return None @@ -2288,7 +2503,7 @@ def dok(self): @property def coo(self): - return np.array([list(k)+[v] for k,v in self._charge2invariant.items()]) + return np.array([list(k) + [v] for k, v in self._charge2invariant.items()]) # misc others # ----------- @@ -2306,9 +2521,10 @@ def size(self): """ return len(self._charge2invariant) -def _group_by_deg(charges: "Iterable", - grading_vec: "ArrayLike", - as_np_arr: bool=False): + +def _group_by_deg( + charges: "Iterable", grading_vec: "ArrayLike", as_np_arr: bool = False +): """ **Description:** Organize the charges by their degrees. @@ -2322,7 +2538,7 @@ def _group_by_deg(charges: "Iterable", *(dictionary)* Dictionary mapping degree to charges. """ charges = np.asarray(sorted(charges)) - degs = charges@grading_vec + degs = charges @ grading_vec # sort degrees sort_inds = np.argsort(degs) @@ -2331,15 +2547,15 @@ def _group_by_deg(charges: "Iterable", # organize as dict charges_by_deg = dict() if as_np_arr: - for charge,deg in zip(charges,degs): - charges_by_deg[deg] = charges_by_deg.get(deg,[]) + [charge] - + for charge, deg in zip(charges, degs): + charges_by_deg[deg] = charges_by_deg.get(deg, []) + [charge] + # map to numpy arrays - for k,v in charges_by_deg.items(): - charges_by_deg[k] = np.asarray(v,dtype=int) + for k, v in charges_by_deg.items(): + charges_by_deg[k] = np.asarray(v, dtype=int) else: - for charge,deg in zip(charges,degs): - charges_by_deg[deg] = charges_by_deg.get(deg,set()) + for charge, deg in zip(charges, degs): + charges_by_deg[deg] = charges_by_deg.get(deg, set()) charges_by_deg[deg].add(tuple(charge)) return charges_by_deg diff --git a/src/cytools/cone.py b/src/cytools/cone.py index fa875e5..94696a9 100644 --- a/src/cytools/cone.py +++ b/src/cytools/cone.py @@ -43,6 +43,7 @@ from cytools import config from cytools.utils import gcd_list, array_fmpz_to_int, array_fmpq_to_float + class Cone: """ This class handles all computations relating to rational polyhedral cones, @@ -93,12 +94,14 @@ class Cone: ``` """ - def __init__(self, - rays: "ArrayLike"=None, - hyperplanes: "ArrayLike"=None, - parse_inputs: bool=True, - check: bool=True, - copy: bool=True): + def __init__( + self, + rays: "ArrayLike" = None, + hyperplanes: "ArrayLike" = None, + parse_inputs: bool = True, + check: bool = True, + copy: bool = True, + ): """ **Description:** Initializes a `Cone` object. @@ -138,8 +141,9 @@ def __init__(self, """ # check whether rays or hyperplanes were input if not ((rays is None) ^ (hyperplanes is None)): - raise ValueError("Exactly one of \"rays\" and \"hyperplanes\" " - "must be specified.") + raise ValueError( + 'Exactly one of "rays" and "hyperplanes" ' "must be specified." + ) # minimal work if we don't parse the data if not parse_inputs: @@ -149,8 +153,9 @@ def __init__(self, self._rays = None data = hyperplanes else: - raise NotImplementedError("Currently, parse_inputs is required " - "if rays are input...") + raise NotImplementedError( + "Currently, parse_inputs is required " "if rays are input..." + ) # initialize other variables self.clear_cache() @@ -187,29 +192,35 @@ def __init__(self, # basic data-checking if len(data.shape) != 2: raise ValueError(f"Input {data_name} must be a 2D matrix.") - elif data.shape[1]<1: + elif data.shape[1] < 1: raise ValueError("Zero-dimensional cones are not supported.") - #elif data.shape[0]<1: + # elif data.shape[0]<1: # raise ValueError(f"At least one {data_name} is required.") self._ambient_dim = data.shape[1] if len(data): # check size of coordinates - if np.min(data)<=-100000000000000: - warnings.warn(f"Extremely small coordinate, {np.min(data)}, " - f"found in {data_name}. Computations may be incorrect.") - if np.max(data)>=+100000000000000: - warnings.warn(f"Extremely large coordinate, {np.max(data)}, " - f"found in {data_name}. Computations may be incorrect.") + if np.min(data) <= -100000000000000: + warnings.warn( + f"Extremely small coordinate, {np.min(data)}, " + f"found in {data_name}. Computations may be incorrect." + ) + if np.max(data) >= +100000000000000: + warnings.warn( + f"Extremely large coordinate, {np.max(data)}, " + f"found in {data_name}. Computations may be incorrect." + ) # parse input according to data type - t = type(data[0,0]) + t = type(data[0, 0]) if t in (fmpz, fmpq): if not config._exp_features_enabled: - raise Exception("Arbitrary precision data types only have " - "experimental support, so experimental " - "features must be enabled in configuration.") + raise Exception( + "Arbitrary precision data types only have " + "experimental support, so experimental " + "features must be enabled in configuration." + ) if t == fmpz: data = array_fmpz_to_int(data) else: @@ -231,17 +242,18 @@ def __init__(self, # reduce by them if t == np.int64: - mask = (gcds > 0) + mask = gcds > 0 if False in mask: - warnings.warn("0 gcd found (row of zeros)... " - "Skipping it!") - data = data[mask]//gcds[mask].reshape(-1,1).astype(int) + warnings.warn("0 gcd found (row of zeros)... " "Skipping it!") + data = data[mask] // gcds[mask].reshape(-1, 1).astype(int) else: - mask = (gcds >= 1e-5) + mask = gcds >= 1e-5 if False in mask: - warnings.warn("Extremely small gcd found... " - "Computations may be incorrect!") - data = np.rint(data[mask]/gcds[mask].reshape(-1,1)).astype(int) + warnings.warn( + "Extremely small gcd found... " + "Computations may be incorrect!" + ) + data = np.rint(data[mask] / gcds[mask].reshape(-1, 1)).astype(int) else: data = data.astype(int) @@ -313,11 +325,15 @@ def __repr__(self): ``` """ if self._rays is not None: - return (f"A {self._dim}-dimensional rational polyhedral cone in " - f"RR^{self._ambient_dim} generated by {len(self._rays)} " - f"rays") - return (f"A rational polyhedral cone in RR^{self._ambient_dim} " - f"defined by {len(self._hyperplanes)} hyperplanes") + return ( + f"A {self._dim}-dimensional rational polyhedral cone in " + f"RR^{self._ambient_dim} generated by {len(self._rays)} " + f"rays" + ) + return ( + f"A rational polyhedral cone in RR^{self._ambient_dim} " + f"defined by {len(self._hyperplanes)} hyperplanes" + ) def __eq__(self, other): """ @@ -347,34 +363,46 @@ def __eq__(self, other): if not isinstance(other, Cone): return NotImplemented - if (self._rays is not None and other._rays is not None and - sorted(self._rays.tolist()) == sorted(other._rays.tolist())): + if ( + self._rays is not None + and other._rays is not None + and sorted(self._rays.tolist()) == sorted(other._rays.tolist()) + ): # rays trivially match # N.B.: doesn't check for non-trivial equivalence. E.g., # self._rays = {e_1, -e_1, e2, -e_2} # other._rays = {e_1+e_2, -(e_1+e_2), e_1-e_2, -(e_1-e_2)} return True - if (self._hyperplanes is not None and other._hyperplanes is not None - and sorted(self._hyperplanes.tolist()) == - sorted(other._hyperplanes.tolist())): + if ( + self._hyperplanes is not None + and other._hyperplanes is not None + and sorted(self._hyperplanes.tolist()) + == sorted(other._hyperplanes.tolist()) + ): # hyperplanes trivially match # N.B.: doesn't check for non-trivial equivalence. Same as above return True if self.is_pointed() ^ other.is_pointed(): return False if self.is_pointed() and other.is_pointed(): - return (sorted(self.extremal_rays().tolist()) - == sorted(other.extremal_rays().tolist())) + return sorted(self.extremal_rays().tolist()) == sorted( + other.extremal_rays().tolist() + ) if self.dual().is_pointed() ^ other.dual().is_pointed(): return False if self.dual().is_pointed() and other.dual().is_pointed(): - return (sorted(self.dual().extremal_rays().tolist()) - == sorted(other.dual().extremal_rays().tolist())) + return sorted(self.dual().extremal_rays().tolist()) == sorted( + other.dual().extremal_rays().tolist() + ) # ugly method... check if each ray self is contained in other # (and vice-versa) - self_contained_in_other = np.all(other.hyperplanes()@self.rays().transpose()>=0) - other_contained_in_self = np.all(self.hyperplanes()@other.rays().transpose()>=0) + self_contained_in_other = np.all( + other.hyperplanes() @ self.rays().transpose() >= 0 + ) + other_contained_in_self = np.all( + self.hyperplanes() @ other.rays().transpose() >= 0 + ) return self_contained_in_other and other_contained_in_self def __ne__(self, other): @@ -435,18 +463,20 @@ def __hash__(self): if self._hash is not None: return self._hash if self.is_pointed(): - self._hash = hash(tuple(sorted(tuple(v) - for v in self.extremal_rays()))) + self._hash = hash(tuple(sorted(tuple(v) for v in self.extremal_rays()))) return self._hash if self.dual().is_pointed(): # Note: The minus sign is important because otherwise the dual cone # would have the same hash. - self._hash = -hash(tuple(sorted(tuple(v) - for v in self.dual().extremal_rays()))) + self._hash = -hash( + tuple(sorted(tuple(v) for v in self.dual().extremal_rays())) + ) return self._hash - warnings.warn("Cones that are not pointed and whose duals are also " - "not pointed are assigned a hash value of 0.") + warnings.warn( + "Cones that are not pointed and whose duals are also " + "not pointed are assigned a hash value of 0." + ) return 0 def ambient_dimension(self): @@ -472,6 +502,7 @@ def ambient_dimension(self): ``` """ return self._ambient_dim + # aliases ambient_dim = ambient_dimension @@ -504,6 +535,7 @@ def dimension(self): return self._dim self._dim = np.linalg.matrix_rank(self.rays()) return self._dim + # aliases dim = dimension @@ -535,15 +567,15 @@ def rays(self): return np.array(self._ext_rays) if self._rays is not None: return np.array(self._rays) - if (self._ambient_dim >= 12 - and len(self._hyperplanes) != self._ambient_dim): - warnings.warn("This operation might take a while for d > ~12 " - "and is likely impossible for d > ~18.") + if self._ambient_dim >= 12 and len(self._hyperplanes) != self._ambient_dim: + warnings.warn( + "This operation might take a while for d > ~12 " + "and is likely impossible for d > ~18." + ) cs = ppl.Constraint_System() vrs = [ppl.Variable(i) for i in range(self._ambient_dim)] for h in self.dual().extremal_rays(): - cs.insert( - sum(h[i]*vrs[i] for i in range(self._ambient_dim)) >= 0 ) + cs.insert(sum(h[i] * vrs[i] for i in range(self._ambient_dim)) >= 0) cone = ppl.C_Polyhedron(cs) rays = [] for gen in cone.minimized_generators(): @@ -585,14 +617,15 @@ def hyperplanes(self): if self._hyperplanes is not None: return np.array(self._hyperplanes) if self._ambient_dim >= 12 and len(self.rays()) != self._ambient_dim: - warnings.warn("This operation might take a while for d > ~12 " - "and is likely impossible for d > ~18.") + warnings.warn( + "This operation might take a while for d > ~12 " + "and is likely impossible for d > ~18." + ) gs = ppl.Generator_System() vrs = [ppl.Variable(i) for i in range(self._ambient_dim)] gs.insert(ppl.point(0)) for r in self.extremal_rays(): - gs.insert(ppl.ray(sum(r[i]*vrs[i] - for i in range(self._ambient_dim)))) + gs.insert(ppl.ray(sum(r[i] * vrs[i] for i in range(self._ambient_dim)))) cone = ppl.C_Polyhedron(gs) hyperplanes = [] for cstr in cone.minimized_constraints(): @@ -601,8 +634,8 @@ def hyperplanes(self): hyperplanes.append(tuple(-int(c) for c in cstr.coefficients())) self._hyperplanes = np.array(hyperplanes, dtype=int) - if len(self._hyperplanes)==0: - self._hyperplanes = np.zeros((0,self._ambient_dim), dtype=int) + if len(self._hyperplanes) == 0: + self._hyperplanes = np.zeros((0, self._ambient_dim), dtype=int) return np.array(self._hyperplanes) @@ -630,7 +663,7 @@ def contains(self, other, eps: float = 0) -> bool: # cast to 2D array, transpose if len(pt.shape) == 1: - pt = pt.reshape(-1,1) + pt = pt.reshape(-1, 1) return_list = False else: # transpose so columns are points @@ -639,10 +672,10 @@ def contains(self, other, eps: float = 0) -> bool: # compute which points are in the cone if len(H): - contained = np.all(H@pt >= eps, axis=0) + contained = np.all(H @ pt >= eps, axis=0) else: contained = [True for _ in range(pt.shape[1])] - + # return if return_list: return tuple(contained) @@ -681,6 +714,7 @@ def dual_cone(self): self._dual = Cone(rays=self.hyperplanes(), check=False) self._dual._dual = self return self._dual + # aliases dual = dual_cone @@ -727,10 +761,12 @@ def extremal_rays(self, tol=1e-4, verbose=False): else: n_threads = cpu_count() elif n_threads > 1 and not self.is_pointed(): - warnings.warn("When finding the extremal rays of a non-pointed " - "cone in parallel, there can be conflicts that end up " - "producing erroneous results. It is highly recommended to " - "use a single thread.") + warnings.warn( + "When finding the extremal rays of a non-pointed " + "cone in parallel, there can be conflicts that end up " + "producing erroneous results. It is highly recommended to " + "use a single thread." + ) current_rays = set(range(rays.shape[0])) ext_rays = set() @@ -742,8 +778,7 @@ def extremal_rays(self, tol=1e-4, verbose=False): # fill list of rays that we're currently checking for i in current_rays: - if (i not in ext_rays - and (i not in error_rays or rechecking_rays)): + if i not in ext_rays and (i not in error_rays or rechecking_rays): checking.append(i) if len(checking) >= n_threads: break @@ -756,12 +791,16 @@ def extremal_rays(self, tol=1e-4, verbose=False): # check each ray (using multiple threads) q = Queue() - As = [np.array([rays[j] for j in current_rays if j!=k],dtype=int).T - for k in checking] + As = [ + np.array([rays[j] for j in current_rays if j != k], dtype=int).T + for k in checking + ] bs = [rays[k] for k in checking] - procs = [Process(target=is_extremal, - args=(As[k],bs[k],k,q,tol)) for k in range(len(checking))] + procs = [ + Process(target=is_extremal, args=(As[k], bs[k], k, q, tol)) + for k in range(len(checking)) + ] for t in procs: t.start() @@ -778,8 +817,10 @@ def extremal_rays(self, tol=1e-4, verbose=False): failed_after_rechecking = True ext_rays.add(checking[res[0]]) elif verbose: - print("Cone.extremal_rays: Minimization failed. Ray " - "will be rechecked later...") + print( + "Cone.extremal_rays: Minimization failed. Ray " + "will be rechecked later..." + ) elif not res[1]: current_rays.remove(checking[res[0]]) else: @@ -789,19 +830,24 @@ def extremal_rays(self, tol=1e-4, verbose=False): error_rays.remove(checking[res[0]]) if verbose: - print("Cone.extremal_rays: Eliminated " + print( + "Cone.extremal_rays: Eliminated " f"{sum(not r[1] for r in results)}. " - f"Current number of rays: {len(current_rays)}") + f"Current number of rays: {len(current_rays)}" + ) if failed_after_rechecking: - warnings.warn("Minimization failed after multiple attempts. " - "Some rays may not be extremal.") + warnings.warn( + "Minimization failed after multiple attempts. " + "Some rays may not be extremal." + ) try: self._ext_rays = rays[list(ext_rays)] except IndexError as e: - raise Exception(f"Dimension/indexing error rays={rays}; "+\ - f"ext_rays={ext_rays}") from e + raise Exception( + f"Dimension/indexing error rays={rays}; " + f"ext_rays={ext_rays}" + ) from e return self._ext_rays def extremal_hyperplanes(self, tol=1e-4, verbose=False): @@ -820,9 +866,16 @@ def extremal_hyperplanes(self, tol=1e-4, verbose=False): """ return self.dual().extremal_rays(tol=tol, verbose=verbose) - def tip_of_stretched_cone(self, c=1, backend=None, check=True, - constraint_error_tol=5e-2, max_iter=10**6, - show_hints=True, verbose=False): + def tip_of_stretched_cone( + self, + c=1, + backend=None, + check=True, + constraint_error_tol=5e-2, + max_iter=10**6, + show_hints=True, + verbose=False, + ): """ **Description:** Finds the tip of the stretched cone. The stretched cone is defined as @@ -883,21 +936,25 @@ def tip_of_stretched_cone(self, c=1, backend=None, check=True, # set the backend backends = (None, "mosek", "osqp", "cvxopt", "glop") if backend not in backends: - raise ValueError("Invalid backend. " - f"The options are: {backends}.") + raise ValueError("Invalid backend. " f"The options are: {backends}.") if backend is None: if self.ambient_dim() < 25: backend = "osqp" else: - backend = ("mosek" if config.mosek_is_activated() and\ - self.ambient_dim() >= 25 else "glop") + backend = ( + "mosek" + if config.mosek_is_activated() and self.ambient_dim() >= 25 + else "glop" + ) elif backend == "mosek" and not config.mosek_is_activated(): - raise Exception("Mosek is not activated. See the advanced usage " - "page on our website to see how to activate it.") + raise Exception( + "Mosek is not activated. See the advanced usage " + "page on our website to see how to activate it." + ) # check backend - if (self.ambient_dim()>=25) and (backend!="mosek") and verbose: + if (self.ambient_dim() >= 25) and (backend != "mosek") and verbose: print(f"The backend {backend} may not work given the large ") print(f"dimension ({self.ambient_dim()}) of the problem...") @@ -907,26 +964,42 @@ def tip_of_stretched_cone(self, c=1, backend=None, check=True, return np.ones(self._ambient_dim) if backend == "glop": - solution = self.find_interior_point(c, backend="glop", - verbose=verbose) - G = -1*sparse.csc_matrix(self.hyperplanes(), dtype=float) + solution = self.find_interior_point(c, backend="glop", verbose=verbose) + G = -1 * sparse.csc_matrix(self.hyperplanes(), dtype=float) else: hp = self._hyperplanes # The problem is defined as: # Minimize (1/2) x.P.x + q.x # Subject to G.x <= h - P = 2*sparse.identity(hp.shape[1], dtype=float, format="csc") + P = 2 * sparse.identity(hp.shape[1], dtype=float, format="csc") q = np.zeros(hp.shape[1], dtype=float) h = np.full(hp.shape[0], -c, dtype=float) - G = -1*sparse.csc_matrix(hp, dtype=float) - settings_dict = ({"scaling":50, "eps_abs":1e-4, "eps_rel":1e-4, "polish":True} if backend=="osqp" - else dict()) - solution = qpsolvers.solve_qp(P,q,G,h, solver=backend, max_iter=max_iter, verbose=verbose, **settings_dict) + G = -1 * sparse.csc_matrix(hp, dtype=float) + settings_dict = ( + { + "scaling": 50, + "eps_abs": 1e-4, + "eps_rel": 1e-4, + "polish": True, + } + if backend == "osqp" + else dict() + ) + solution = qpsolvers.solve_qp( + P, + q, + G, + h, + solver=backend, + max_iter=max_iter, + verbose=verbose, + **settings_dict, + ) # parse solution if solution is None: if show_hints: - print("Calculated 'solution' was None...", end=' ') + print("Calculated 'solution' was None...", end=" ") print("some potential reasons why:") # max_iter @@ -934,22 +1007,34 @@ def tip_of_stretched_cone(self, c=1, backend=None, check=True, print(f"-) maybe max_iter={max_iter} was too low?") # bad solver - if (self.ambient_dim()>=25) and (backend!="mosek"): - print(f"-) given the high dimension, {self.ambient_dim()}", end=' ') - print(f"and backend={backend}, this could be a numerical", end=' ') - print( "issue. Try Mosek...") + if (self.ambient_dim() >= 25) and (backend != "mosek"): + print( + f"-) given the high dimension, {self.ambient_dim()}", + end=" ", + ) + print( + f"and backend={backend}, this could be a numerical", + end=" ", + ) + print("issue. Try Mosek...") # scaling - print(f"-) if the cone is narrow, try decreasing c from {c}", end=' ') - print("(you can then scale up the tip to hit the desired stretching...)") - + print( + f"-) if the cone is narrow, try decreasing c from {c}", + end=" ", + ) + print( + "(you can then scale up the tip to hit the desired stretching...)" + ) print("For more info, re-run with verbose=True") return if check: res = max(G.dot(solution)) + c if res > constraint_error_tol: - warnings.warn(f"The solution that was found is invalid: {res} > {constraint_error_tol}") + warnings.warn( + f"The solution that was found is invalid: {res} > {constraint_error_tol}" + ) return return solution @@ -980,12 +1065,18 @@ def find_grading_vector(self, backend=None): ``` """ if not self.is_pointed(): - raise Exception("Grading vectors are only defined for pointed " - "cones.") + raise Exception("Grading vectors are only defined for pointed " "cones.") return self.dual().find_interior_point(backend=backend, integral=True) - def find_interior_point(self, c=1, integral=False, backend=None, - check=True, show_hints=False, verbose=False): + def find_interior_point( + self, + c=1, + integral=False, + backend=None, + check=True, + show_hints=False, + verbose=False, + ): """ **Description:** Finds a point in the strict interior of the cone. If no point is found @@ -1019,8 +1110,7 @@ def find_interior_point(self, c=1, integral=False, backend=None, """ backends = (None, "glop", "scip", "cpsat", "mosek", "osqp", "cvxopt") if backend not in backends: - raise ValueError("Invalid backend. " - f"The options are: {backends}.") + raise ValueError("Invalid backend. " f"The options are: {backends}.") # If the rays are already computed then this is a simple task if self._rays is not None and backend is None: @@ -1029,73 +1119,82 @@ def find_interior_point(self, c=1, integral=False, backend=None, point = self._rays.sum(axis=0) - if max(abs(point))>1e-3: + if max(abs(point)) > 1e-3: point //= gcd_list(point) else: # looks like the point is all zeros - if np.prod(self.hyperplanes().shape)==0: + if np.prod(self.hyperplanes().shape) == 0: # trivial cone... all space point = [0 for _ in range(self._ambient_dim)] point[0] = 1 return np.asarray(point) else: - raise Exception(f"Unexpected error in finding point in cone with rays = {self._rays}") + raise Exception( + f"Unexpected error in finding point in cone with rays = {self._rays}" + ) if not integral: - point = point/len(self._rays) + point = point / len(self._rays) return point # Otherwise we need to do a harder computation... if backend is None: - if config.mosek_is_activated() and (self.ambient_dim()>=25): + if config.mosek_is_activated() and (self.ambient_dim() >= 25): backend = "mosek" else: backend = "glop" if backend in ("glop", "scip", "cpsat"): - solution = feasibility(hyperplanes=self._hyperplanes, - c=c, - ambient_dim=self._ambient_dim, - backend=backend, - verbose=verbose) + solution = feasibility( + hyperplanes=self._hyperplanes, + c=c, + ambient_dim=self._ambient_dim, + backend=backend, + verbose=verbose, + ) else: - solution = self.tip_of_stretched_cone(c, - backend=backend, - show_hints=show_hints, - verbose=verbose) + solution = self.tip_of_stretched_cone( + c, backend=backend, show_hints=show_hints, verbose=verbose + ) if solution is None: return None # function to take dot products if isinstance(self._hyperplanes, (list, np.ndarray)): - dot = lambda hp,x: hp.dot(x) + dot = lambda hp, x: hp.dot(x) else: - dot = lambda hp,x: sum([val*x[ind] for ind,val in hp.items()]) + dot = lambda hp, x: sum([val * x[ind] for ind, val in hp.items()]) # Make sure that the solution is valid - if check and any(dot(v,solution) <= 0 for v in self._hyperplanes): + if check and any(dot(v, solution) <= 0 for v in self._hyperplanes): warnings.warn("The solution that was found is invalid.") return None # Finally, round to an integer if necessary if integral: n_tries = 1000 - for i in range(1,n_tries): - int_sol = np.array([int(round(x)) for x in i*solution]) - if all(dot(v,int_sol) > 0 for v in self._hyperplanes): + for i in range(1, n_tries): + int_sol = np.array([int(round(x)) for x in i * solution]) + if all(dot(v, int_sol) > 0 for v in self._hyperplanes): break - if i == n_tries-1: + if i == n_tries - 1: return None solution = int_sol return solution - def find_lattice_points(self, min_points=None, max_deg=None, - grading_vector=None, max_coord=1000, - deg_window=0, - filter_function=None, process_function=None, - verbose=True): + def find_lattice_points( + self, + min_points=None, + max_deg=None, + grading_vector=None, + max_coord=1000, + deg_window=0, + filter_function=None, + process_function=None, + verbose=True, + ): """ **Description:** Finds lattice points in the cone. The points are found in the region @@ -1108,7 +1207,7 @@ def find_lattice_points(self, min_points=None, max_deg=None, - `min_point` *(int, optional)*: Specifies the minimum number of points to find. The degree will be increased until this minimum number is achieved. - - `max_deg` *(int, optional)*: The maximum degree of the points to + - `max_deg` *(int, optional)*: The maximum degree of the points to find. This is useful when working with a preferred grading. - `grading_vector` *(array_like, optional)*: The grading vector that will be used. If it is not specified then it is computed. @@ -1171,14 +1270,17 @@ def process_function(pt): """ # initial checks if max_deg is None and min_points is None: - raise Exception("Either the maximum degree or the minimum number of points must be specified.") + raise Exception( + "Either the maximum degree or the minimum number of points must be specified." + ) if not self.is_pointed(): raise Exception("Only pointed cones are currently supported.") if process_function is not None and filter_function is not None: - raise Exception("Only one of filter_function or process_function " - "can be specified.") + raise Exception( + "Only one of filter_function or process_function " "can be specified." + ) if grading_vector is None: grading_vector = self.find_grading_vector() if max_coord is None: @@ -1233,23 +1335,26 @@ def on_soln_callback_process(self): model = cp_model.CpModel() # define variables - var = [model.NewIntVar(-max_coord, max_coord, f"x_{i}") for i - in range(hp.shape[1])] + var = [ + model.NewIntVar(-max_coord, max_coord, f"x_{i}") + for i in range(hp.shape[1]) + ] # define constraints for v in hp: - model.Add(sum(ii*var[i] for i,ii in enumerate(v)) >= 0) - model.Add(sum(ii*var[i] for i,ii in enumerate(grading_vector))<= 0) - + model.Add(sum(ii * var[i] for i, ii in enumerate(v)) >= 0) + model.Add(sum(ii * var[i] for i, ii in enumerate(grading_vector)) <= 0) + SolutionStorage.on_solution_callback = on_soln_callback_single_pt - solution_storage = SolutionStorage(var, filter_function,\ - process_function) + solution_storage = SolutionStorage(var, filter_function, process_function) try: status = solver.SearchForAllSolutions(model, solution_storage) except MoreThanOneSolution: - raise Exception("More than one solution was found. The grading" - " vector must be wrong.") + raise Exception( + "More than one solution was found. The grading" + " vector must be wrong." + ) # Now, construct the solution storage that will hold the points we find if filter_function is not None: @@ -1259,52 +1364,56 @@ def on_soln_callback_process(self): else: SolutionStorage.on_solution_callback = on_soln_callback_default - solution_storage = SolutionStorage(var, filter_function,\ - process_function) + solution_storage = SolutionStorage(var, filter_function, process_function) # define the model solver = cp_model.CpSolver() model = cp_model.CpModel() # define variables - var = [model.NewIntVar(-max_coord, max_coord, f"x_{i}") - for i in range(hp.shape[1])] + var = [ + model.NewIntVar(-max_coord, max_coord, f"x_{i}") for i in range(hp.shape[1]) + ] # define constraints for h in hp: - model.Add(sum(ii*var[i] for i,ii in enumerate(h)) >= 0) + model.Add(sum(ii * var[i] for i, ii in enumerate(h)) >= 0) - soln_deg = sum(ii*var[i] for i,ii in enumerate(grading_vector)) + soln_deg = sum(ii * var[i] for i, ii in enumerate(grading_vector)) # solve according to whether max_deg or min_points was specified if max_deg is not None: # If the maximum degree is specified, we use it as a constraint model.Add(soln_deg <= max_deg) - + # solve and check status status = solver.SearchForAllSolutions(model, solution_storage) if status != cp_model.OPTIMAL: - print("There was a problem finding the points. Status code: " - f"{solver.StatusName(status)}") + print( + "There was a problem finding the points. Status code: " + f"{solver.StatusName(status)}" + ) return else: # Else, add points until the minimum number is reached deg = 0 - while solution_storage._n_sol= 0) + cs.insert(sum(h[i] * vrs[i] for i in range(self._ambient_dim)) >= 0) cone = ppl.C_Polyhedron(cs) self._is_solid = cone.affine_dimension() == self._ambient_dim @@ -1381,7 +1496,7 @@ def is_solid(self, backend=None): # Otherwise we check this by trying to find an interior point interior_point = self.find_interior_point(show_hints=False, backend=backend) self._is_solid = interior_point is not None - + return self._is_solid # aliases @@ -1430,14 +1545,15 @@ def is_pointed(self, backend=None, tol=1e-7): if backend == "nnls" and self._rays is not None: # If the cone is defined in term of hyperplanes we still don't use # nnls since we first would have to find the rays. - A = np.empty((self._rays.shape[1]+1,self._rays.shape[0]),dtype=int) - A[:-1,:] = self._rays.T - A[-1,:] = 1 - b = [0]*self._rays.shape[1] + [1] - self._is_pointed = nnls(A,b)[1] > tol + A = np.empty((self._rays.shape[1] + 1, self._rays.shape[0]), dtype=int) + A[:-1, :] = self._rays.T + A[-1, :] = 1 + b = [0] * self._rays.shape[1] + [1] + self._is_pointed = nnls(A, b)[1] > tol return self._is_pointed self._is_pointed = self.dual().is_solid(backend=backend) return self._is_pointed + # aliases is_strongly_convex = is_pointed @@ -1497,13 +1613,12 @@ def is_smooth(self): self._is_smooth = False return self._is_smooth if self.is_solid(): - self._is_smooth = (abs(abs(np.linalg.det(self.extremal_rays()))-1) - < 1e-4) + self._is_smooth = abs(abs(np.linalg.det(self.extremal_rays())) - 1) < 1e-4 return self._is_smooth - snf = np.array(fmpz_mat(self.extremal_rays().tolist()).snf().tolist(), - dtype=int) - self._is_smooth = (abs(np.prod([snf[i,i] for i in range(len(snf))])) - == 1) + snf = np.array( + fmpz_mat(self.extremal_rays().tolist()).snf().tolist(), dtype=int + ) + self._is_smooth = abs(np.prod([snf[i, i] for i in range(len(snf))])) == 1 return self._is_smooth def hilbert_basis(self): @@ -1534,17 +1649,27 @@ def hilbert_basis(self): # Generate a random project name so that it doesn't conflict with # other computations letters = string.ascii_lowercase - proj_name = "cytools_" + "".join( random.choice(letters)\ - for i in range(10) ) + proj_name = "cytools_" + "".join(random.choice(letters) for i in range(10)) rays = self.rays() with open(f"/dev/shm/{proj_name}.in", "w+") as f: f.write(f"amb_space {rays.shape[1]}\ncone {rays.shape[0]}\n") - f.write(str(rays.tolist()).replace("],","\n").replace(",","").replace("[","").replace("]","")+"\n") - - normaliz = subprocess.Popen( ("normaliz", f"/dev/shm/{proj_name}.in"), - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, universal_newlines=True ) + f.write( + str(rays.tolist()) + .replace("],", "\n") + .replace(",", "") + .replace("[", "") + .replace("]", "") + + "\n" + ) + + normaliz = subprocess.Popen( + ("normaliz", f"/dev/shm/{proj_name}.in"), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) normaliz_out = normaliz.communicate() with open(f"/dev/shm/{proj_name}.out", "r") as f: data = f.readlines() @@ -1567,14 +1692,14 @@ def hilbert_basis(self): if "lattice points in polytope" in l or "Hilbert basis elements" in l: n_rays = literal_eval(l.split()[0]) for i in range(n_rays): - rays.append([literal_eval(c) for c in data[l_n+1+i].split()]) - l_n += n_rays+1 + rays.append([literal_eval(c) for c in data[l_n + 1 + i].split()]) + l_n += n_rays + 1 continue if "further Hilbert basis elements" in l: n_rays = literal_eval(l.split()[0]) for i in range(n_rays): - rays.append([literal_eval(c) for c in data[l_n+1+i].split()]) - l_n += n_rays+1 + rays.append([literal_eval(c) for c in data[l_n + 1 + i].split()]) + l_n += n_rays + 1 continue l_n += 1 continue @@ -1605,16 +1730,16 @@ def intersection(self, other): ``` """ if isinstance(other, Cone): - return Cone(hyperplanes=self.hyperplanes().tolist() - + other.hyperplanes().tolist() ) + return Cone( + hyperplanes=self.hyperplanes().tolist() + other.hyperplanes().tolist() + ) hyperplanes = self.hyperplanes().tolist() for c in other: if not isinstance(c, Cone): raise ValueError("Elements of the list must be Cone objects.") if c.ambient_dim() != self.ambient_dim(): - raise ValueError("Ambient lattices must have the same" - "dimension.") + raise ValueError("Ambient lattices must have the same" "dimension.") hyperplanes.extend(c.hyperplanes().tolist()) return Cone(hyperplanes=hyperplanes) @@ -1652,21 +1777,24 @@ def is_extremal(A, b, i=None, q=None, tol=1e-4): ``` """ try: - v = nnls(A,b) + v = nnls(A, b) is_ext = abs(v[1]) > tol if q is not None: q.put((i, is_ext)) return is_ext except: if q is not None: - q.put((i,None)) + q.put((i, None)) return -def feasibility(hyperplanes: "ArrayLike", - c: float, - ambient_dim: int, - backend: str, - verbose: bool = False): + +def feasibility( + hyperplanes: "ArrayLike", + c: float, + ambient_dim: int, + backend: str, + verbose: bool = False, +): """ **Description:** Solve a feasibility problem Ax>=c. @@ -1685,10 +1813,10 @@ def feasibility(hyperplanes: "ArrayLike", hyperplanes = np.asarray(hyperplanes) hp_iter = enumerate else: - hp_iter = lambda hp:hp.items() + hp_iter = lambda hp: hp.items() # accomodate trivial hyperplanes - if len(hyperplanes)==0: + if len(hyperplanes) == 0: return np.ones(ambient_dim) if backend in ("glop", "scip"): @@ -1700,23 +1828,22 @@ def feasibility(hyperplanes: "ArrayLike", # define variables var = [] - var_type = (solver.NumVar if backend=="glop" else solver.IntVar) + var_type = solver.NumVar if backend == "glop" else solver.IntVar for i in range(ambient_dim): - var.append((var_type)(-solver.infinity(), solver.infinity(),\ - f"x_{i}")) + var.append((var_type)(-solver.infinity(), solver.infinity(), f"x_{i}")) # define constraints cons_list = [] for v in hyperplanes: cons_list.append(solver.Constraint(c, solver.infinity())) - for ind,val in hp_iter(v): + for ind, val in hp_iter(v): cons_list[-1].SetCoefficient(var[ind], float(val)) # define objective obj = solver.Objective() obj.SetMinimization() - obj_vec = hyperplanes.sum(axis=0)/len(hyperplanes) + obj_vec = hyperplanes.sum(axis=0) / len(hyperplanes) for i in range(ambient_dim): obj.SetCoefficient(var[i], obj_vec[i]) @@ -1736,23 +1863,25 @@ def feasibility(hyperplanes: "ArrayLike", "UNBOUNDED", "ABNORMAL", "MODEL_INVALID", - "NOT_SOLVED"] + "NOT_SOLVED", + ] warnings.warn(f"Solver returned status {status_list[status]}.") return None - elif backend=="cpsat": + elif backend == "cpsat": solver = cp_model.CpSolver() model = cp_model.CpModel() # define variables var = [] for i in range(ambient_dim): - var.append(model.NewIntVar(cp_model.INT32_MIN,\ - cp_model.INT32_MAX, f"x_{i}")) + var.append( + model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, f"x_{i}") + ) # define constraints for v in hyperplanes: - model.Add(sum(ii*var[i] for i,ii in enumerate(v)) >= c) + model.Add(sum(ii * var[i] for i, ii in enumerate(v)) >= c) # define objective obj_vec = hyperplanes.sum(axis=0) @@ -1760,7 +1889,7 @@ def feasibility(hyperplanes: "ArrayLike", obj = 0 for i in range(ambient_dim): - obj += var[i]*obj_vec[i] + obj += var[i] * obj_vec[i] model.Minimize(obj) @@ -1771,7 +1900,6 @@ def feasibility(hyperplanes: "ArrayLike", elif status == cp_model.INFEASIBLE: return None else: - warnings.warn("Solver returned status " - f"{solver.StatusName(status)}.") + warnings.warn("Solver returned status " f"{solver.StatusName(status)}.") return solution diff --git a/src/cytools/config.py b/src/cytools/config.py index 888498d..2586423 100644 --- a/src/cytools/config.py +++ b/src/cytools/config.py @@ -34,9 +34,13 @@ palp_path = "/usr/bin/" # Mosek license -_mosek_license = f"/home/{'root' if os.geteuid()==0 else 'cytools'}/mounted_volume/mosek/mosek.lic" +_mosek_license = ( + f"/home/{'root' if os.geteuid()==0 else 'cytools'}/mounted_volume/mosek/mosek.lic" +) _mosek_is_activated = None _mosek_error = "" + + def check_mosek_license(silent=False): """ **Description:** @@ -64,22 +68,28 @@ def check_mosek_license(silent=False): global _mosek_is_activated try: import mosek - mosek.Env().Task(0,0).optimize() + + mosek.Env().Task(0, 0).optimize() _mosek_is_activated = True if not silent: print("Mosek was successfully activated.") except mosek.Error as e: - _mosek_error = ("Info: Mosek is not activated. " - "An alternative optimizer will be used.\n" - f"Error encountered: {e}") + _mosek_error = ( + "Info: Mosek is not activated. " + "An alternative optimizer will be used.\n" + f"Error encountered: {e}" + ) _mosek_is_activated = False except: - _mosek_error = ("Info: There was a problem with Mosek. " - "An alternative optimizer will be used.") + _mosek_error = ( + "Info: There was a problem with Mosek. " + "An alternative optimizer will be used." + ) _mosek_is_activated = False if not silent: print(_mosek_error) + def mosek_is_activated(): global _mosek_error global _mosek_is_activated @@ -88,6 +98,7 @@ def mosek_is_activated(): check_mosek_license(silent=True) return _mosek_is_activated + def set_mosek_path(path): """ **Description:** @@ -115,9 +126,11 @@ def set_mosek_path(path): _mosek_license = path check_mosek_license() + # Lock experimental features by default. _exp_features_enabled = False + def enable_experimental_features(): """ **Description:** @@ -139,8 +152,10 @@ def enable_experimental_features(): """ global _exp_features_enabled _exp_features_enabled = True - warnings.warn("\n**************************************************************\n" - "Warning: You have enabled experimental features of CYTools.\n" - "Some of these features may be broken or not fully tested,\n" - "and they may undergo significant changes in future versions.\n" - "**************************************************************\n") + warnings.warn( + "\n**************************************************************\n" + "Warning: You have enabled experimental features of CYTools.\n" + "Some of these features may be broken or not fully tested,\n" + "and they may undergo significant changes in future versions.\n" + "**************************************************************\n" + ) diff --git a/src/cytools/h_polytope/h_polytope.py b/src/cytools/h_polytope/h_polytope.py index acb6d0e..f469c10 100644 --- a/src/cytools/h_polytope/h_polytope.py +++ b/src/cytools/h_polytope/h_polytope.py @@ -30,6 +30,7 @@ from cytools import Polytope from cytools.utils import gcd_list + class HPolytope(Polytope): """ This class handles all computations relating to H-polytopes. These are not @@ -63,11 +64,13 @@ class HPolytope(Polytope): otherwise. """ - def __init__(self, - ineqs: "ArrayLike" = None, - dilate: bool = False, - backend: str=None, - verbosity: int=0) -> None: + def __init__( + self, + ineqs: "ArrayLike" = None, + dilate: bool = False, + backend: str = None, + verbosity: int = 0, + ) -> None: """ **Description:** Initializes a `HPolytope` object describing a lattice polytope. @@ -104,24 +107,26 @@ def __init__(self, if dilate: # dilate so that the vertices are all integral - gcd = gcd_list(self._real_vertices.flatten()) - points = np.rint(self._real_vertices/gcd).astype(int) + gcd = gcd_list(self._real_vertices.flatten()) + points = np.rint(self._real_vertices / gcd).astype(int) else: # get the contained lattice points points = lattice_points(self._real_vertices, self._ineqs) - if len(points)==0: - error_msg = "No lattice points in the Polytope! "\ - +f"The real-valued vertices are {self._real_vertices.tolist()}..., "\ - +f"defined from inequalities {self._ineqs.tolist()}..." + if len(points) == 0: + error_msg = ( + "No lattice points in the Polytope! " + + f"The real-valued vertices are {self._real_vertices.tolist()}..., " + + f"defined from inequalities {self._ineqs.tolist()}..." + ) raise ValueError(error_msg) # run Polytope initializer super().__init__(points=points, backend=backend) + # utils # ----- -def poly_h_to_v(hypers: "ArrayLike", - verbosity: int = 0) -> ("ArrayLike", None): +def poly_h_to_v(hypers: "ArrayLike", verbosity: int = 0) -> ("ArrayLike", None): """ **Description:** Generate the V-representation of a polytope, given the H-representation. @@ -130,7 +135,7 @@ def poly_h_to_v(hypers: "ArrayLike", The inequalities, c, must organized as a matrix for which each row is an inequality of the form c[i,0] * x_0 + ... + c[i,d-1] * x_{d-1} + c[i,d] >= 0 - + Only works with ppl backend, currently. **Arguments:** @@ -140,11 +145,11 @@ def poly_h_to_v(hypers: "ArrayLike", **Returns:** The associated points of the polytope and the formal convex hull. """ - hypers = np.array(hypers) # don't use .asarray so as to ensure we copy them - + hypers = np.array(hypers) # don't use .asarray so as to ensure we copy them + # preliminary - dim = len(hypers[0])-1 - + dim = len(hypers[0]) - 1 + # scale hyperplanes to be integral if hypers.dtype != int: if verbosity >= 1: @@ -152,8 +157,8 @@ def poly_h_to_v(hypers: "ArrayLike", # divide by GCD for i in range(len(hypers)): - hypers[i,:] /= gcd_list(hypers[i,:]) - + hypers[i, :] /= gcd_list(hypers[i, :]) + # round/cast to int hypers = np.rint(hypers).astype(int) @@ -163,7 +168,7 @@ def poly_h_to_v(hypers: "ArrayLike", # insert points to generator system for c in hypers: - cs.insert(sum(c[i]*vrs[i] for i in range(dim)) + c[-1] >= 0) + cs.insert(sum(c[i] * vrs[i] for i in range(dim)) + c[-1] >= 0) # find polytope, vertices # ----------------------- @@ -176,18 +181,18 @@ def poly_h_to_v(hypers: "ArrayLike", return div = int(pt.divisor()) - if div==1: + if div == 1: # handle this separately to maintain integer typing pts.append([int(coeff) for coeff in pt.coefficients()]) else: - pts.append([int(coeff)/div for coeff in pt.coefficients()]) + pts.append([int(coeff) / div for coeff in pt.coefficients()]) pts = np.array(pts) # return return pts, poly -def lattice_points(verts: "ArrayLike", - ineqs: "ArrayLike") -> "ArrayLike": + +def lattice_points(verts: "ArrayLike", ineqs: "ArrayLike") -> "ArrayLike": """ **Description:** Enumerate all lattice points in a polytope with given vertices and @@ -209,19 +214,19 @@ def lattice_points(verts: "ArrayLike", """ # output variable _lattice_pts = [] - + # basic helper variables dim = len(verts[0]) - + # find bounding box for the lattice points box_min = np.ceil(np.min(verts, axis=0)).astype(int) box_max = np.floor(np.max(verts, axis=0)).astype(int) - + # try all lattice points x = np.empty(dim, dtype=int) - for dx in itertools.product(*list(map(range, box_max-box_min+1))): - x = box_min+dx # the point to try - if all(ineqs[:,:-1]@x + ineqs[:,-1] >= 0): + for dx in itertools.product(*list(map(range, box_max - box_min + 1))): + x = box_min + dx # the point to try + if all(ineqs[:, :-1] @ x + ineqs[:, -1] >= 0): # it passes all inequality checks! _lattice_pts.append(x.tolist()) diff --git a/src/cytools/polytope.py b/src/cytools/polytope.py index cdaedf9..6059aac 100644 --- a/src/cytools/polytope.py +++ b/src/cytools/polytope.py @@ -38,11 +38,15 @@ # CYTools imports from cytools import config from cytools.polytopeface import PolytopeFace -from cytools.triangulation import (Triangulation, all_triangulations, - random_triangulations_fast_generator, - random_triangulations_fair_generator) +from cytools.triangulation import ( + Triangulation, + all_triangulations, + random_triangulations_fast_generator, + random_triangulations_fair_generator, +) from cytools.utils import gcd_list, lll_reduce, instanced_lru_cache + class Polytope: """ This class handles all computations relating to lattice polytopes, such as @@ -89,10 +93,9 @@ class Polytope: ``` """ - def __init__(self, - points: ArrayLike, - labels: ArrayLike = None, - backend: str=None) -> None: + def __init__( + self, points: ArrayLike, labels: ArrayLike = None, backend: str = None + ) -> None: """ **Description:** Initializes a `Polytope` object describing a lattice polytope. @@ -136,8 +139,8 @@ def __init__(self, N_unique_pts = len({tuple(pt) for pt in points}) if N_input_pts != N_unique_pts: msg = f"Points must all be unique! Out of {N_input_pts} points, " - msg +=f"only {N_unique_pts} of them were unique...\n" - msg +=f"Points = {points}..." + msg += f"only {N_unique_pts} of them were unique...\n" + msg += f"Points = {points}..." raise ValueError(msg) @@ -147,19 +150,24 @@ def __init__(self, N_unique_labels = len(set(labels)) if N_labels != N_unique_labels: - raise ValueError("Labels must all be unique! " +\ - f"There were {N_labels}, {N_unique_labels} "+\ - "of them were unique...") + raise ValueError( + "Labels must all be unique! " + + f"There were {N_labels}, {N_unique_labels} " + + "of them were unique..." + ) if N_labels != N_input_pts: - raise ValueError(f"Count of labels, {N_labels}, must match " +\ - f"the count of points, {N_input_pts}") + raise ValueError( + f"Count of labels, {N_labels}, must match " + + f"the count of points, {N_input_pts}" + ) # check that backend is allowed backends = ["ppl", "qhull", "palp", None] if backend not in backends: - raise ValueError(f"Invalid backend, {backend}."+\ - f" Options are {backends}.") + raise ValueError( + f"Invalid backend, {backend}." + f" Options are {backends}." + ) # initialize attributes # --------------------- @@ -169,8 +177,8 @@ def __init__(self, # ------------------ # dimension self._dim_ambient = len(points[0]) - self._dim = np.linalg.matrix_rank([list(pt)+[1] for pt in points]) - 1 - self._dim_diff = self.ambient_dim()-self.dim() + self._dim = np.linalg.matrix_rank([list(pt) + [1] for pt in points]) - 1 + self._dim_diff = self.ambient_dim() - self.dim() # backend if backend is None: @@ -179,7 +187,7 @@ def __init__(self, else: backend = "palp" - if self.dim() == 0: # 0-dimensional polytopes are finicky + if self.dim() == 0: # 0-dimensional polytopes are finicky backend = "palp" self._backend = backend @@ -209,11 +217,13 @@ def __repr__(self) -> str: # A 4-dimensional reflexive lattice polytope in ZZ^4 ``` """ - return (f"A {self.dim()}-dimensional " - f"{('reflexive ' if self.is_reflexive() else '')}" - f"lattice polytope in ZZ^{self.ambient_dim()} " - f"with points {list(self._inputpts2labels.keys())} " - f"which are labelled {list(self._inputpts2labels.values())}") + return ( + f"A {self.dim()}-dimensional " + f"{('reflexive ' if self.is_reflexive() else '')}" + f"lattice polytope in ZZ^{self.ambient_dim()} " + f"with points {list(self._inputpts2labels.keys())} " + f"which are labelled {list(self._inputpts2labels.values())}" + ) def __str__(self) -> str: """ @@ -236,9 +246,11 @@ def __str__(self) -> str: # A 4-dimensional reflexive lattice polytope in ZZ^4 ``` """ - return (f"A {self.dim()}-dimensional " - f"{('reflexive ' if self.is_reflexive() else '')}" - f"lattice polytope in ZZ^{self.ambient_dim()}") + return ( + f"A {self.dim()}-dimensional " + f"{('reflexive ' if self.is_reflexive() else '')}" + f"lattice polytope in ZZ^{self.ambient_dim()}" + ) def __getstate__(self): """ @@ -253,7 +265,7 @@ def __getstate__(self): """ state = self.__dict__.copy() # delete instanced_lru_cache since it doesn't play nicely with pickle - state['_cache'] = None + state["_cache"] = None return state def __setstate__(self, state: dict): @@ -297,7 +309,7 @@ def __eq__(self, other: "Polytope") -> bool: our_verts = self.vertices().tolist() other_verts = other.vertices().tolist() - return(sorted(our_verts) == sorted(other_verts)) + return sorted(our_verts) == sorted(other_verts) def __ne__(self, other: "Polytope") -> bool: """ @@ -319,7 +331,7 @@ def __ne__(self, other: "Polytope") -> bool: # False ``` """ - return(not self.__eq__(other)) + return not self.__eq__(other) def __hash__(self) -> int: """ @@ -414,70 +426,70 @@ def clear_cache(self) -> None: # basics # ------ # inputs (DON'T CLEAR! Set in init...) - #self._backend + # self._backend # simple properties - self._hash = None - self._volume = None + self._hash = None + self._volume = None # points # ------ # LLL-reduction (DON'T CLEAR! Set in init...) - #self._transf_mat_inv - #self._transl_vector + # self._transf_mat_inv + # self._transl_vector # H-rep (DON'T CLEAR! Set in init...) - #self._ineqs_input - #self._ineqs_optimal - #self._poly_optimal - self._is_reflexive = None + # self._ineqs_input + # self._ineqs_optimal + # self._poly_optimal + self._is_reflexive = None # input, optimal points (DON'T CLEAR! Set in init...) - #self._labels2inputPts - #self._labels2optPts - #self._pts_saturating + # self._labels2inputPts + # self._labels2optPts + # self._pts_saturating - #self._pts_order + # self._pts_order - #self._inputpts2labels - #self._optimalpts2labels - #self._labels2inds + # self._inputpts2labels + # self._optimalpts2labels + # self._labels2inds # groups of points - #self._label_origin = None - #self._labels_int = None - #self._labels_facet = None - #self._labels_bdry = None - #self._labels_codim2 = None - #self._labels_not_facet = None - self._labels_vertices = None + # self._label_origin = None + # self._labels_int = None + # self._labels_facet = None + # self._labels_bdry = None + # self._labels_codim2 = None + # self._labels_not_facet = None + self._labels_vertices = None # others # ------ # dimension (DON'T CLEAR! Set in init...) - #self._dim - #self._dim_ambient - #self._dim_diff + # self._dim + # self._dim_ambient + # self._dim_diff # dual, faces, H-rep - self._dual = None - self._faces = None + self._dual = None + self._faces = None # symmetries - self._autos = [None]*4 + self._autos = [None] * 4 # hodge self._chi = None - self._is_favorable = None + self._is_favorable = None # glsm - self._glsm_basis = dict() - self._glsm_charge_matrix = dict() - self._glsm_linrels = dict() - + self._glsm_basis = dict() + self._glsm_charge_matrix = dict() + self._glsm_linrels = dict() + # misc - self._nef_parts = dict() - self._normal_form = [None]*3 + self._nef_parts = dict() + self._normal_form = [None] * 3 # getters # ======= @@ -519,6 +531,7 @@ def ambient_dimension(self) -> int: ``` """ return self._dim_ambient + # aliases ambient_dim = ambient_dimension @@ -545,6 +558,7 @@ def dimension(self) -> int: ``` """ return self._dim + # aliases dim = dimension @@ -568,7 +582,7 @@ def is_solid(self) -> bool: # False ``` """ - return(self.ambient_dim() == self.dim()) + return self.ambient_dim() == self.dim() @property def labels(self): @@ -600,7 +614,7 @@ def labels_facet(self): @property def labels_bdry(self): return self._labels_bdry - + @property def labels_codim2(self): return self._labels_codim2 @@ -608,15 +622,15 @@ def labels_codim2(self): @property def labels_not_facet(self): return self._labels_not_facet - + def inequalities(self) -> np.ndarray: """ **Description:** Returns the inequalities giving the hyperplane representation of the polytope. The inequalities are given in the form - + $c_0x_0 + \cdots + c_{d-1}x_{d-1} + c_d \geq 0$. - + Note, however, that equalities are not included. **Arguments:** @@ -643,9 +657,7 @@ def inequalities(self) -> np.ndarray: # ====== # internal/prep # ------------- - def _process_points(self, - pts_input: ArrayLike, - labels: ArrayLike = None) -> None: + def _process_points(self, pts_input: ArrayLike, labels: ArrayLike = None) -> None: """ **Description:** Internal function for processing input points. Should only be called @@ -673,11 +685,11 @@ def _process_points(self, self._transl_vector = np.zeros(self.ambient_dim(), dtype=int) else: self._transl_vector = pts_input[0] - pts_optimal = np.array(pts_input)-self._transl_vector + pts_optimal = np.array(pts_input) - self._transl_vector # LLL-reduction (allows reduction in dimension) - pts_optimal, transf =lll_reduce(pts_optimal, transform=True) - pts_optimal = pts_optimal[:, self._dim_diff:] + pts_optimal, transf = lll_reduce(pts_optimal, transform=True) + pts_optimal = pts_optimal[:, self._dim_diff :] transf_mat, self._transf_mat_inv = transf # Calculate the polytope, inequalities @@ -687,27 +699,27 @@ def _process_points(self, # convert to input representation shape_opt = self._ineqs_optimal.shape - shape = (shape_opt[0], shape_opt[1]+self._dim_diff) + shape = (shape_opt[0], shape_opt[1] + self._dim_diff) self._ineqs_input = np.zeros(shape, dtype=int) - self._ineqs_input[:,self._dim_diff:] = self._ineqs_optimal - self._ineqs_input[:,:-1]=transf_mat.T.dot(self._ineqs_input[:,:-1].T).T + self._ineqs_input[:, self._dim_diff :] = self._ineqs_optimal + self._ineqs_input[:, :-1] = transf_mat.T.dot(self._ineqs_input[:, :-1].T).T if self.is_solid(): - self._ineqs_input[:,-1] = self._ineqs_optimal[:,-1] + self._ineqs_input[:, -1] = self._ineqs_optimal[:, -1] else: # this method is always correct, just a bit slower - for i,v in enumerate(self._ineqs_input): - self._ineqs_input[i,-1] = self._ineqs_optimal[i,-1] \ - - v[:-1].dot(self._transl_vector) + for i, v in enumerate(self._ineqs_input): + self._ineqs_input[i, -1] = self._ineqs_optimal[i, -1] - v[:-1].dot( + self._transl_vector + ) # Get the lattice points and their saturated inequalities # ------------------------------------------------------- pts_optimal = [tuple(pt) for pt in pts_optimal] - pts_optimal_all, saturating = saturating_lattice_pts(pts_optimal, - self._ineqs_optimal, - self.dim(), - self._backend) + pts_optimal_all, saturating = saturating_lattice_pts( + pts_optimal, self._ineqs_optimal, self.dim(), self._backend + ) # undo LLL transformation, to get points in original basis pts_input_all = self._optimal_to_input(pts_optimal_all) @@ -728,7 +740,7 @@ def sort_fct(ind): if len(saturating[ind]) > 0: out.append(-len(saturating[ind])) else: - out.append(-float('inf')) + out.append(-float("inf")) # the coordinates out += list(pts_input_all[ind]) @@ -740,7 +752,7 @@ def sort_fct(ind): # save info to useful variables/dictionaries self._labels2optPts = dict() self._pts_saturating = dict() - nSat_to_labels = [[] for _ in range(len(self._ineqs_optimal)+1)] + nSat_to_labels = [[] for _ in range(len(self._ineqs_optimal) + 1)] if labels is None: labels = [] @@ -750,10 +762,10 @@ def sort_fct(ind): pt = tuple(pts_optimal_all[i]) # find the label to use - if (labels!=[]) and (pt in pts_optimal): + if (labels != []) and (pt in pts_optimal): label = labels[pts_optimal.index(pt)] else: - label = last_default_label+1 + label = last_default_label + 1 while (label in self._labels2optPts) or (label in labels): label += 1 @@ -765,49 +777,50 @@ def sort_fct(ind): nSat_to_labels[len(saturating[i])].append(label) # save order of labels - self._pts_order = sum(nSat_to_labels[1:][::-1], - nSat_to_labels[0]) - #if hasattr(self._pts_order[0], 'item'): + self._pts_order = sum(nSat_to_labels[1:][::-1], nSat_to_labels[0]) + # if hasattr(self._pts_order[0], 'item'): # # convert numpy types to ordinary ones # self._pts_order = tuple([i.item() for i in self._pts_order]) - #else: + # else: # self._pts_order = tuple([i for i in self._pts_order]) - self._pts_order = tuple([i.item() if hasattr(i, 'item') else i\ - for i in self._pts_order]) + self._pts_order = tuple( + [i.item() if hasattr(i, "item") else i for i in self._pts_order] + ) # dictionary from labels to input coordinates pts_input_all = self._optimal_to_input(self.points(optimal=True)) - self._labels2inputPts = {label:tuple(pt) for label,pt in \ - zip(self._pts_order, pts_input_all)} + self._labels2inputPts = { + label: tuple(pt) for label, pt in zip(self._pts_order, pts_input_all) + } # reverse dictionaries - self._inputpts2labels ={v:k for k,v in self._labels2inputPts.items()} - self._optimalpts2labels ={v:k for k,v in self._labels2optPts.items()} + self._inputpts2labels = {v: k for k, v in self._labels2inputPts.items()} + self._optimalpts2labels = {v: k for k, v in self._labels2optPts.items()} - self._labels2inds ={v:i for i,v in enumerate(self._pts_order)} + self._labels2inds = {v: i for i, v in enumerate(self._pts_order)} # common sets of labels # --------------------- - origin = (0,)*self.ambient_dim() + origin = (0,) * self.ambient_dim() if origin in self._inputpts2labels: self._label_origin = self._inputpts2labels[origin] else: self._label_origin = None - self._labels_int = nSat_to_labels[0] - self._labels_facet = nSat_to_labels[1] + self._labels_int = nSat_to_labels[0] + self._labels_facet = nSat_to_labels[1] - self._labels_bdry = sum(nSat_to_labels[1:][::-1],[]) - self._labels_codim2 = sum(nSat_to_labels[2:][::-1],[]) + self._labels_bdry = sum(nSat_to_labels[1:][::-1], []) + self._labels_codim2 = sum(nSat_to_labels[2:][::-1], []) self._labels_not_facet = self._labels_int + self._labels_codim2 # store as tuples - self._labels_int = tuple(self._labels_int) - self._labels_facet = tuple(self._labels_facet) - self._labels_bdry = tuple(self._labels_bdry) - self._labels_codim2 = tuple(self._labels_codim2) - self._labels_not_facet = tuple(self._labels_not_facet) + self._labels_int = tuple(self._labels_int) + self._labels_facet = tuple(self._labels_facet) + self._labels_bdry = tuple(self._labels_bdry) + self._labels_codim2 = tuple(self._labels_codim2) + self._labels_not_facet = tuple(self._labels_not_facet) def _optimal_to_input(self, pts_opt: ArrayLike) -> np.array: """ @@ -833,25 +846,24 @@ def _optimal_to_input(self, pts_opt: ArrayLike) -> np.array: # pad points with 0s, to make width match original dim points_orig = np.empty((len(pts_opt), self.ambient_dim()), dtype=int) - points_orig[:,self._dim_diff:] = pts_opt - points_orig[:,:self._dim_diff] = 0 + points_orig[:, self._dim_diff :] = pts_opt + points_orig[:, : self._dim_diff] = 0 # undo the LLL-reduction points_orig = self._transf_mat_inv.dot(points_orig.T).T # undo the translation, if applicable if not self.is_solid(): - for i in range(len( points_orig )): - points_orig[i,:] += self._transl_vector + for i in range(len(points_orig)): + points_orig[i, :] += self._transl_vector return points_orig # main # ---- - def points(self, - which = None, - optimal: bool = False, - as_indices: bool = False) -> np.ndarray: + def points( + self, which=None, optimal: bool = False, as_indices: bool = False + ) -> np.ndarray: """ **Description:** Returns the lattice points of the polytope. @@ -900,7 +912,7 @@ def points(self, # get the labels of the points to return if which is None: which = self._pts_order - elif (not isinstance(which,np.ndarray)) and (which in self._pts_order): + elif (not isinstance(which, np.ndarray)) and (which in self._pts_order): which = [which] # return the answer in the desired format @@ -912,39 +924,48 @@ def points(self, pts = self._labels2optPts else: pts = self._labels2inputPts - + # return return np.array([pts[label] for label in which]) + # aliases pts = points # common point grabbers # --------------------- - pts_int = lambda self, as_indices=False:\ - self.pts(which=self._labels_int, as_indices=as_indices) - pts_bdry = lambda self, as_indices=False:\ - self.pts(which=self._labels_bdry, as_indices=as_indices) - pts_facet = lambda self, as_indices=False:\ - self.pts(which=self._labels_facet, as_indices=as_indices) - pts_codim2 = lambda self, as_indices=False:\ - self.pts(which=self._labels_codim2, as_indices=as_indices) - pts_not_facets = lambda self, as_indices=False:\ - self.pts(which=self._labels_not_facet, as_indices=as_indices) + pts_int = lambda self, as_indices=False: self.pts( + which=self._labels_int, as_indices=as_indices + ) + pts_bdry = lambda self, as_indices=False: self.pts( + which=self._labels_bdry, as_indices=as_indices + ) + pts_facet = lambda self, as_indices=False: self.pts( + which=self._labels_facet, as_indices=as_indices + ) + pts_codim2 = lambda self, as_indices=False: self.pts( + which=self._labels_codim2, as_indices=as_indices + ) + pts_not_facets = lambda self, as_indices=False: self.pts( + which=self._labels_not_facet, as_indices=as_indices + ) # aliases - interior_points = pts_int; interior_pts = pts_int - boundary_points = pts_bdry; bdry_points = pts_bdry - points_interior_to_facets = pts_facet; pts_interior_to_facets = pts_facet + interior_points = pts_int + interior_pts = pts_int + boundary_points = pts_bdry + bdry_points = pts_bdry + points_interior_to_facets = pts_facet + pts_interior_to_facets = pts_facet - boundary_points_not_interior_to_facets = pts_codim2 - boundary_pts_not_interior_to_facets = pts_codim2 + boundary_points_not_interior_to_facets = pts_codim2 + boundary_pts_not_interior_to_facets = pts_codim2 - points_not_interior_to_facets = pts_not_facets - pts_not_interior_to_facets = pts_not_facets + points_not_interior_to_facets = pts_not_facets + pts_not_interior_to_facets = pts_not_facets - def points_to_labels(self, - points: ArrayLike, - is_optimal: bool = False) -> "list | None": + def points_to_labels( + self, points: ArrayLike, is_optimal: bool = False + ) -> "list | None": """ **Description:** Returns the list of labels corresponding to the given points. It also @@ -961,11 +982,11 @@ def points_to_labels(self, the point if only one is given. """ # check for empty input - if len(points)==0: + if len(points) == 0: return [] # map single-point input into list case - single_pt = (len(np.array(points).shape) == 1) + single_pt = len(np.array(points).shape) == 1 if single_pt: points = [points] @@ -978,13 +999,13 @@ def points_to_labels(self, # get/return the indices labels = [relevant_map[tuple(pt)] for pt in points] if single_pt and len(labels): - return labels[0] # just return the single label + return labels[0] # just return the single label else: - return labels # return a list of labels + return labels # return a list of labels - def points_to_indices(self, - points: ArrayLike, - is_optimal: bool = False) -> "np.ndarray | int": + def points_to_indices( + self, points: ArrayLike, is_optimal: bool = False + ) -> "np.ndarray | int": """ **Description:** Returns the list of indices corresponding to the given points. It also @@ -1011,27 +1032,25 @@ def points_to_indices(self, ``` """ # check for empty input - if len(points)==0: - return np.asarray([],dtype=int) + if len(points) == 0: + return np.asarray([], dtype=int) # map single-point input into list case - single_pt = (len(np.array(points).shape) == 1) + single_pt = len(np.array(points).shape) == 1 if single_pt: points = [points] # grab labels, and then map to indices labels = self.points_to_labels(points, is_optimal=is_optimal) inds = self.points(which=labels, as_indices=True) - + # get/return the indices if single_pt and len(inds): return inds[0] # just return the single index else: - return inds # return a list of indices + return inds # return a list of indices - def vertices(self, - optimal: bool = False, - as_indices: bool = False) -> np.ndarray: + def vertices(self, optimal: bool = False, as_indices: bool = False) -> np.ndarray: """ **Description:** Returns the vertices of the polytope. @@ -1059,9 +1078,11 @@ def vertices(self, """ # return the answer if known if self._labels_vertices is not None: - return self.pts(which=self._labels_vertices, - optimal=optimal, - as_indices=as_indices) + return self.pts( + which=self._labels_vertices, + optimal=optimal, + as_indices=as_indices, + ) # calculate the answer if self.dim() == 0: @@ -1069,7 +1090,7 @@ def vertices(self, self._labels_vertices = self._pts_order elif self._backend == "qhull": - if self.dim() == 1: # QHull cannot handle 1D polytopes + if self.dim() == 1: # QHull cannot handle 1D polytopes self._labels_vertices = self._labels_facet else: verts = self._poly_optimal.points[self._poly_optimal.vertices] @@ -1081,39 +1102,46 @@ def vertices(self, verts.append(pt.coefficients()) verts = np.array(verts, dtype=int) - else: # Backend is PALP - palp = subprocess.Popen((config.palp_path + "poly.x", "-v"), - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True) + else: # Backend is PALP + palp = subprocess.Popen( + (config.palp_path + "poly.x", "-v"), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) # prep input for PALP pt_list = "" pts_optimal = {tuple(pt) for pt in self.points(optimal=True)} for pt in pts_optimal: - pt_list += (str(pt).replace("(","").replace(")","").replace(","," ") + "\n") + pt_list += ( + str(pt).replace("(", "").replace(")", "").replace(",", " ") + + "\n" + ) # do the work - palp_out = palp.communicate(input=f"{len(pts_optimal)} {self.dim()}\n" + pt_list + "\n")[0] + palp_out = palp.communicate( + input=f"{len(pts_optimal)} {self.dim()}\n" + pt_list + "\n" + )[0] if "Vertices of P" not in palp_out: raise RuntimeError(f"PALP error. Full output: {palp_out}") # read the outputs palp_out = palp_out.split("\n") - for i,line in enumerate(palp_out): + for i, line in enumerate(palp_out): if "Vertices of P" not in line: continue pts_shape = [int(c) for c in line.split()[:2]] tmp_pts = np.empty(pts_shape, dtype=int) for j in range(pts_shape[0]): - tmp_pts[j,:] =[int(c) for c in palp_out[i+j+1].split()] + tmp_pts[j, :] = [int(c) for c in palp_out[i + j + 1].split()] break - verts = (tmp_pts.T if pts_shape[0] < pts_shape[1] else tmp_pts) + verts = tmp_pts.T if pts_shape[0] < pts_shape[1] else tmp_pts # for either ppl/PALP, map points to original representation if self._labels_vertices is None: - self._labels_vertices =self.points_to_labels(verts,is_optimal=True) + self._labels_vertices = self.points_to_labels(verts, is_optimal=True) # sort, map to tuple self._labels_vertices = tuple(sorted(self._labels_vertices)) @@ -1160,12 +1188,12 @@ def faces(self, d: int = None) -> tuple: ``` """ # input checking - if (d is not None) and (d not in range(self.dim()+1)): + if (d is not None) and (d not in range(self.dim() + 1)): raise ValueError(f"Polytope does not have faces of dimension {d}") # return answer if known if self._faces is not None: - return (self._faces[d] if (d is not None) else self._faces) + return self._faces[d] if (d is not None) else self._faces # calculate the answer # ==================== @@ -1177,12 +1205,11 @@ def faces(self, d: int = None) -> tuple: # codim>0 faces in increasing order of dimension for dim_faces in self._dual._faces[::-1][1:]: - self._faces.append( tuple(f.dual() for f in dim_faces) ) + self._faces.append(tuple(f.dual() for f in dim_faces)) # full-dim face - self._faces.append( (PolytopeFace(self, \ - self._labels_vertices, \ - frozenset(), \ - dim=self._dim), ) ) + self._faces.append( + (PolytopeFace(self, self._labels_vertices, frozenset(), dim=self._dim),) + ) # cast to tuple self._faces = tuple(self._faces) @@ -1199,18 +1226,16 @@ def faces(self, d: int = None) -> tuple: # --------------------------------- # get vertices, along with their saturated inequalities verts = [tuple(pt) for pt in self.vertices()] - vert_sat = [self._pts_saturating[label] for label in\ - self.labels_vertices] + vert_sat = [self._pts_saturating[label] for label in self.labels_vertices] vert_legacy = list(zip(verts, vert_sat)) # construct faces in reverse order (decreasing dim) self._faces = [] # full-dim face - self._faces.append( (PolytopeFace(self, \ - self.labels_vertices, \ - frozenset(), \ - dim=self.dim()), ) ) + self._faces.append( + (PolytopeFace(self, self.labels_vertices, frozenset(), dim=self.dim()),) + ) # if polytope is 0-dimensional, we're done! if self.dim() == 0: self._faces = tuple(self._faces) @@ -1223,11 +1248,11 @@ def faces(self, d: int = None) -> tuple: # dim-dd) to the points saturating them # # then, to get dim-(dd-1) faces, just take intersections of dim-dd ones - for dd in range(self.dim()-1, 0, -1): + for dd in range(self.dim() - 1, 0, -1): # map from inequalities to points saturating them ineq2pts = defaultdict(set) - if dd == self.dim()-1: + if dd == self.dim() - 1: # facets... for f-th facet, just collect all points saturating # said inequality for pt in vert_legacy: @@ -1235,10 +1260,10 @@ def faces(self, d: int = None) -> tuple: ineq2pts[frozenset([f])].add(pt) else: # codim>1 faces... take intersections of higher-dim faces - for f1, f2 in itertools.combinations(ineq2pts_prev.values(),2): + for f1, f2 in itertools.combinations(ineq2pts_prev.values(), 2): # check if their intersection has the right dimension inter = f1 & f2 - dim = np.linalg.matrix_rank([pt[0]+(1,) for pt in inter])-1 + dim = np.linalg.matrix_rank([pt[0] + (1,) for pt in inter]) - 1 if dim != dd: continue @@ -1250,18 +1275,22 @@ def faces(self, d: int = None) -> tuple: dd_faces = [] for f in ineq2pts.keys(): tmp_vert = [pt[0] for pt in vert_legacy if f.issubset(pt[1])] - dd_faces.append(PolytopeFace(self, self.points_to_labels(tmp_vert), f, dim=dd)) + dd_faces.append( + PolytopeFace(self, self.points_to_labels(tmp_vert), f, dim=dd) + ) self._faces.append(dd_faces) # store for next iteration ineq2pts_prev = ineq2pts - + # Finally add vertices - self._faces.append([PolytopeFace(self, - self.points_to_labels([pt[0]]), - pt[1], - dim=0) for pt in vert_legacy]) + self._faces.append( + [ + PolytopeFace(self, self.points_to_labels([pt[0]]), pt[1], dim=0) + for pt in vert_legacy + ] + ) # reverse order (to increasing with dimension) self._faces = tuple(tuple(ff) for ff in self._faces[::-1]) @@ -1300,8 +1329,7 @@ def _faces4d(self) -> tuple: # get vertices, along with their saturated inequalities verts = [tuple(pt) for pt in self.vertices()] - vert_sat = [self._pts_saturating[label] for label in \ - self.labels_vertices] + vert_sat = [self._pts_saturating[label] for label in self.labels_vertices] vert_legacy = list(zip(verts, vert_sat)) # facets @@ -1312,17 +1340,17 @@ def _faces4d(self) -> tuple: # 2-faces twofaces = defaultdict(set) - for ineqs1, ineqs2 in itertools.combinations(facets.keys(),2): - inter = facets[ineqs1] & facets[ineqs2] + for ineqs1, ineqs2 in itertools.combinations(facets.keys(), 2): + inter = facets[ineqs1] & facets[ineqs2] - # These intersections are 2D iff there are at least 3 vertices. - if len(inter) >= 3: - ineqs3 = ineqs1 | ineqs2 - twofaces[ineqs3] = inter + # These intersections are 2D iff there are at least 3 vertices. + if len(inter) >= 3: + ineqs3 = ineqs1 | ineqs2 + twofaces[ineqs3] = inter # 1-faces onefaces = defaultdict(set) - for f1, f2 in itertools.combinations(twofaces.values(),2): + for f1, f2 in itertools.combinations(twofaces.values(), 2): inter = f1 & f2 # These intersections are 1D iff there are exactly 2 vertices. @@ -1334,35 +1362,44 @@ def _faces4d(self) -> tuple: onefaces[f3] = inter # now, construct all formal face objects - fourface_obj_list = [PolytopeFace(self, - self.labels_vertices, - frozenset(), - dim=4)] + fourface_obj_list = [ + PolytopeFace(self, self.labels_vertices, frozenset(), dim=4) + ] facets_obj_list = [] for f in facets.keys(): - tmp_vert = self.points_to_labels([pt[0] for pt in vert_legacy if f.issubset(pt[1])]) + tmp_vert = self.points_to_labels( + [pt[0] for pt in vert_legacy if f.issubset(pt[1])] + ) facets_obj_list.append(PolytopeFace(self, tmp_vert, f, dim=3)) twofaces_obj_list = [] for f in twofaces.keys(): - tmp_vert = self.points_to_labels([pt[0] for pt in vert_legacy if f.issubset(pt[1])]) + tmp_vert = self.points_to_labels( + [pt[0] for pt in vert_legacy if f.issubset(pt[1])] + ) twofaces_obj_list.append(PolytopeFace(self, tmp_vert, f, dim=2)) onefaces_obj_list = [] for f in onefaces.keys(): - tmp_vert = self.points_to_labels([pt[0] for pt in vert_legacy if f.issubset(pt[1])]) + tmp_vert = self.points_to_labels( + [pt[0] for pt in vert_legacy if f.issubset(pt[1])] + ) onefaces_obj_list.append(PolytopeFace(self, tmp_vert, f, dim=1)) - zerofaces_obj_list = [PolytopeFace(self, - self.points_to_labels([pt[0]]), - pt[1], - dim=0) for pt in vert_legacy] + zerofaces_obj_list = [ + PolytopeFace(self, self.points_to_labels([pt[0]]), pt[1], dim=0) + for pt in vert_legacy + ] # organize in tuple and return - organized_faces = (tuple(zerofaces_obj_list), tuple(onefaces_obj_list), - tuple(twofaces_obj_list), tuple(facets_obj_list), - tuple(fourface_obj_list)) + organized_faces = ( + tuple(zerofaces_obj_list), + tuple(onefaces_obj_list), + tuple(twofaces_obj_list), + tuple(facets_obj_list), + tuple(fourface_obj_list), + ) return organized_faces def facets(self) -> tuple[PolytopeFace]: @@ -1383,7 +1420,7 @@ def facets(self) -> tuple[PolytopeFace]: facets = p.facets() ``` """ - return self.faces(self.dim()-1) + return self.faces(self.dim() - 1) # H-rep, dual # =========== @@ -1428,13 +1465,15 @@ def dual_polytope(self) -> "Polytope": # calculate the answer if not self.is_reflexive(): - raise NotImplementedError("Duality of non-reflexive polytopes "+\ - "is not supported.") + raise NotImplementedError( + "Duality of non-reflexive polytopes " + "is not supported." + ) - pts = np.array(self._ineqs_input[:,:-1]) + pts = np.array(self._ineqs_input[:, :-1]) self._dual = Polytope(pts, backend=self._backend) self._dual._dual = self return self._dual + # aliases dual = dual_polytope polar_polytope = dual_polytope @@ -1464,18 +1503,21 @@ def is_reflexive(self) -> bool: return self._is_reflexive # calculate the answer - self._is_reflexive = self.is_solid() and\ - all(c == 1 for c in self._ineqs_input[:,-1]) + self._is_reflexive = self.is_solid() and all( + c == 1 for c in self._ineqs_input[:, -1] + ) # return return self._is_reflexive # symmetries # ========== - def automorphisms(self, - square_to_one: bool = False, - action: str = "right", - as_dictionary: bool = False) -> "np.ndarray | dict": + def automorphisms( + self, + square_to_one: bool = False, + action: str = "right", + as_dictionary: bool = False, + ) -> "np.ndarray | dict": """ **Description:** Returns the $SL^{\pm}(d,\mathbb{Z})$ matrices that leave the polytope @@ -1550,14 +1592,16 @@ def automorphisms(self, """ # check that this is sensical if self.dim() != self.ambient_dim(): - raise NotImplementedError("Automorphisms can only be computed " +\ - "for full-dimensional polytopes.") - + raise NotImplementedError( + "Automorphisms can only be computed " + + "for full-dimensional polytopes." + ) + if action not in ("right", "left"): - raise ValueError("Options for action are \"right\" or \"left\".") + raise ValueError('Options for action are "right" or "left".') # check if we know the answer - args_id = 1*square_to_one + 2*as_dictionary + args_id = 1 * square_to_one + 2 * as_dictionary if self._autos[args_id] is not None: if as_dictionary: return copy.deepcopy(self._autos[args_id]) @@ -1571,15 +1615,17 @@ def automorphisms(self, vert_set = set(tuple(pt) for pt in self.vertices()) # get the facet with minimum number of vertices - f_min = min(self.facets(), key=lambda f:len(f.vertices())) - f_min_vert_rref = np.array(fmpz_mat(f_min.vertices().T.tolist()).hnf().tolist(), dtype=int) + f_min = min(self.facets(), key=lambda f: len(f.vertices())) + f_min_vert_rref = np.array( + fmpz_mat(f_min.vertices().T.tolist()).hnf().tolist(), dtype=int + ) pivots = [] for v in f_min_vert_rref: if not any(v): continue - for i,ii in enumerate(v): + for i, ii in enumerate(v): if ii != 0: pivots.append(i) break @@ -1595,15 +1641,22 @@ def automorphisms(self, autos2 = [] for im in images: image = fmpz_mat(im) - m = basis_inverse*image + m = basis_inverse * image if not all(abs(c.q) == 1 for c in np.array(m.tolist()).flatten()): continue - m = np.array([[int(c.p)//int(c.q) for c in r] # just in case c.q==-1 by some weird reason - for r in np.array(m.tolist())], dtype=int) + m = np.array( + [ + [ + int(c.p) // int(c.q) for c in r + ] # just in case c.q==-1 by some weird reason + for r in np.array(m.tolist()) + ], + dtype=int, + ) if set(tuple(pt) for pt in np.dot(self.vertices(), m)) != vert_set: continue autos.append(m) - if all((np.dot(m,m) == np.eye(self.dim(), dtype=int)).flatten()): + if all((np.dot(m, m) == np.eye(self.dim(), dtype=int)).flatten()): autos2.append(m) self._autos[0] = np.array(autos) self._autos[1] = np.array(autos2) @@ -1613,10 +1666,14 @@ def automorphisms(self, pts_tup = [tuple(pt) for pt in self.points()] for a in self._autos[0]: new_pts_tup = [tuple(pt) for pt in self.points().dot(a)] - autos_dict.append({i:new_pts_tup.index(ii) for i,ii in enumerate(pts_tup)}) + autos_dict.append( + {i: new_pts_tup.index(ii) for i, ii in enumerate(pts_tup)} + ) for a in self._autos[1]: new_pts_tup = [tuple(pt) for pt in self.points().dot(a)] - autos2_dict.append({i:new_pts_tup.index(ii) for i,ii in enumerate(pts_tup)}) + autos2_dict.append( + {i: new_pts_tup.index(ii) for i, ii in enumerate(pts_tup)} + ) self._autos[2] = autos_dict self._autos[3] = autos2_dict @@ -1628,9 +1685,9 @@ def automorphisms(self, else: return np.array(self._autos[args_id]) - def normal_form(self, - affine_transform: bool = False, - backend: str = "palp") -> np.ndarray: + def normal_form( + self, affine_transform: bool = False, backend: str = "palp" + ) -> np.ndarray: """ **Description:** Returns the normal form of the polytope as defined by Kreuzer-Skarke. @@ -1676,33 +1733,41 @@ def normal_form(self, # process backend if backend not in ("native", "palp"): - raise ValueError("Error: options for backend are \"native\" and " - "\"palp\".") + raise ValueError('Error: options for backend are "native" and ' '"palp".') if backend == "palp": if not self.is_solid(): - warnings.warn("PALP doesn't support polytopes that are not " - "full-dimensional. Using native backend.") + warnings.warn( + "PALP doesn't support polytopes that are not " + "full-dimensional. Using native backend." + ) backend = "native" # check if we know the answer - args_id = 1*affine_transform + affine_transform*(backend=="native")*1 + args_id = 1 * affine_transform + affine_transform * (backend == "native") * 1 if self._normal_form[args_id] is not None: return np.array(self._normal_form[args_id]) # PALP backend # ------------ if backend == "palp": - palp = subprocess.Popen((config.palp_path + "poly.x", ("-A" if affine_transform else "-N")), - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, universal_newlines=True) - + palp = subprocess.Popen( + ( + config.palp_path + "poly.x", + ("-A" if affine_transform else "-N"), + ), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + # prep the command pt_list = "" pts_optimal = {tuple(pt) for pt in self.points(optimal=True)} for pt in pts_optimal: - pt_str = str(pt).replace("(","").replace(")","") - pt_str = pt_str.replace(","," ") + pt_str = str(pt).replace("(", "").replace(")", "") + pt_str = pt_str.replace(",", " ") pt_list += pt_str + "\n" palp_in = f"{len(pts_optimal)} {self.dim()}\n{pt_list}\n" @@ -1719,9 +1784,9 @@ def normal_form(self, pts_shape = [int(c) for c in palp_out[i].split()[:2]] tmp_pts = np.empty(pts_shape, dtype=int) for j in range(pts_shape[0]): - tmp_pts[j,:] = [int(c) for c in palp_out[i+j+1].split()] + tmp_pts[j, :] = [int(c) for c in palp_out[i + j + 1].split()] break - points = (tmp_pts.T if pts_shape[0] < pts_shape[1] else tmp_pts) + points = tmp_pts.T if pts_shape[0] < pts_shape[1] else tmp_pts # cache it and return self._normal_form[args_id] = points @@ -1734,10 +1799,10 @@ def PGE(n, u, v): tmp_m = np.eye(n, dtype=int) if u == v: return tmp_m - tmp_m[u-1,u-1] = 0 - tmp_m[v-1,v-1] = 0 - tmp_m[u-1,v-1] = 1 - tmp_m[v-1,u-1] = 1 + tmp_m[u - 1, u - 1] = 0 + tmp_m[v - 1, v - 1] = 0 + tmp_m[u - 1, v - 1] = 1 + tmp_m[v - 1, u - 1] = 1 return tmp_m V = self.vertices() @@ -1745,38 +1810,43 @@ def PGE(n, u, v): n_f = len(self._ineqs_input) PM = np.array([n[:-1].dot(V.T) + n[-1] for n in self._ineqs_input]) n_s = 1 - prm = {0 : [np.eye(n_f, dtype=int), np.eye(n_v, dtype=int)]} + prm = {0: [np.eye(n_f, dtype=int), np.eye(n_v, dtype=int)]} for j in range(n_v): - m = np.argmax([PM[0,prm[0][1].dot(range(n_v))][i] for i in range(j, n_v)]) + m = np.argmax([PM[0, prm[0][1].dot(range(n_v))][i] for i in range(j, n_v)]) if m > 0: - prm[0][1] = PGE(n_v, j+1, m+j+1).dot(prm[0][1]) + prm[0][1] = PGE(n_v, j + 1, m + j + 1).dot(prm[0][1]) first_row = list(PM[0]) # Arrange other rows one by one and compare with first row for k in range(1, n_f): # Error for k == 1 already! prm[n_s] = [np.eye(n_f, dtype=int), np.eye(n_v, dtype=int)] - m = np.argmax(PM[:,prm[n_s][1].dot(range(n_v))][k]) + m = np.argmax(PM[:, prm[n_s][1].dot(range(n_v))][k]) if m > 0: - prm[n_s][1] = PGE(n_v, 1, m+1).dot(prm[n_s][1]) - d = PM[k,prm[n_s][1].dot(range(n_v))][0] - prm[0][1].dot(first_row)[0] + prm[n_s][1] = PGE(n_v, 1, m + 1).dot(prm[n_s][1]) + d = PM[k, prm[n_s][1].dot(range(n_v))][0] - prm[0][1].dot(first_row)[0] if d < 0: # The largest elt of this row is smaller than largest elt # in 1st row, so nothing to do continue # otherwise: for i in range(1, n_v): - m = np.argmax([PM[k,prm[n_s][1].dot(range(n_v))][j] for j in range(i, n_v)]) + m = np.argmax( + [PM[k, prm[n_s][1].dot(range(n_v))][j] for j in range(i, n_v)] + ) if m > 0: - prm[n_s][1] = PGE(n_v, i+1, m+i+1).dot(prm[n_s][1]) + prm[n_s][1] = PGE(n_v, i + 1, m + i + 1).dot(prm[n_s][1]) if d == 0: - d = PM[k,prm[n_s][1].dot(range(n_v))][i] - prm[0][1].dot(first_row)[i] + d = ( + PM[k, prm[n_s][1].dot(range(n_v))][i] + - prm[0][1].dot(first_row)[i] + ) if d < 0: break if d < 0: # This row is smaller than 1st row, so nothing to do del prm[n_s] continue - prm[n_s][0] = PGE(n_f, 1, k+1).dot(prm[n_s][0]) + prm[n_s][0] = PGE(n_f, 1, k + 1).dot(prm[n_s][0]) if d == 0: # This row is the same, so we have a symmetry! n_s += 1 @@ -1786,31 +1856,31 @@ def PGE(n, u, v): first_row = list(PM[k]) prm = {0: prm[n_s]} n_s = 1 - prm = {k:prm[k] for k in prm if k < n_s} - b = PM[prm[0][0].dot(range(n_f)),:][:,prm[0][1].dot(range(n_v))][0] + prm = {k: prm[k] for k in prm if k < n_s} + b = PM[prm[0][0].dot(range(n_f)), :][:, prm[0][1].dot(range(n_v))][0] # Work out the restrictions the current permutations # place on other permutations as a automorphisms # of the first row # The array is such that: # S = [i, 1, ..., 1 (ith), j, i+1, ..., i+1 (jth), k ... ] # describes the "symmetry blocks" - S = list(range(1, n_v+1)) + S = list(range(1, n_v + 1)) for i in range(1, n_v): - if b[i-1] == b[i]: - S[i] = S[i-1] - S[S[i]-1] += 1 + if b[i - 1] == b[i]: + S[i] = S[i - 1] + S[S[i] - 1] += 1 else: S[i] = i + 1 # We determine the other rows of PM_max in turn by use of perms and # aut on previous rows. - for l in range(1, n_f-1): + for l in range(1, n_f - 1): n_s = len(prm) n_s_bar = n_s cf = 0 - l_r = [0]*n_v + l_r = [0] * n_v # Search for possible local permutations based off previous # global permutations. - for k in range(n_s_bar-1, -1, -1): + for k in range(n_s_bar - 1, -1, -1): # number of local permutations associated with current global n_p = 0 ccf = cf @@ -1820,34 +1890,40 @@ def PGE(n, u, v): # between 0 and S(0) for s in range(l, n_f): for j in range(1, S[0]): - v = PM[prmb[n_p][0].dot(range(n_f)),:][:,prmb[n_p][1].dot(range(n_v))][s] + v = PM[prmb[n_p][0].dot(range(n_f)), :][ + :, prmb[n_p][1].dot(range(n_v)) + ][s] if v[0] < v[j]: - prmb[n_p][1] = PGE(n_v, 1, j+1).dot(prmb[n_p][1]) + prmb[n_p][1] = PGE(n_v, 1, j + 1).dot(prmb[n_p][1]) if ccf == 0: - l_r[0] = PM[prmb[n_p][0].dot(range(n_f)),:][:,prmb[n_p][1].dot(range(n_v))][s,0] - prmb[n_p][0] = PGE(n_f, l+1, s+1).dot(prmb[n_p][0]) + l_r[0] = PM[prmb[n_p][0].dot(range(n_f)), :][ + :, prmb[n_p][1].dot(range(n_v)) + ][s, 0] + prmb[n_p][0] = PGE(n_f, l + 1, s + 1).dot(prmb[n_p][0]) n_p += 1 ccf = 1 prmb[n_p] = copy.copy(prm[k]) else: - d1 = PM[prmb[n_p][0].dot(range(n_f)),:][:,prmb[n_p][1].dot(range(n_v))][s,0] + d1 = PM[prmb[n_p][0].dot(range(n_f)), :][ + :, prmb[n_p][1].dot(range(n_v)) + ][s, 0] d = d1 - l_r[0] if d < 0: # We move to the next line continue if d == 0: # Maximal values agree, so possible symmetry - prmb[n_p][0] = PGE(n_f, l+1, s+1).dot(prmb[n_p][0]) + prmb[n_p][0] = PGE(n_f, l + 1, s + 1).dot(prmb[n_p][0]) n_p += 1 prmb[n_p] = copy.copy(prm[k]) else: # We found a greater maximal value for first entry. # It becomes our new reference: l_r[0] = d1 - prmb[n_p][0] = PGE(n_f, l+1, s+1).dot(prmb[n_p][0]) + prmb[n_p][0] = PGE(n_f, l + 1, s + 1).dot(prmb[n_p][0]) # Forget previous work done cf = 0 - prmb = {0:copy.copy(prmb[n_p])} + prmb = {0: copy.copy(prmb[n_p])} n_p = 1 prmb[n_p] = copy.copy(prm[k]) n_s = k + 1 @@ -1858,23 +1934,29 @@ def PGE(n, u, v): ccf = cf # Now let us find out where the end of the # next symmetry block is: - if h < c+1: - h = S[h-1] + if h < c + 1: + h = S[h - 1] s = n_p # Check through this block for each possible permutation while s > 0: s -= 1 # Find the largest value in this symmetry block - for j in range(c+1, h): - v = PM[prmb[s][0].dot(range(n_f)),:][:,prmb[s][1].dot(range(n_v))][l] + for j in range(c + 1, h): + v = PM[prmb[s][0].dot(range(n_f)), :][ + :, prmb[s][1].dot(range(n_v)) + ][l] if v[c] < v[j]: - prmb[s][1] = PGE(n_v, c+1, j+1).dot(prmb[s][1]) + prmb[s][1] = PGE(n_v, c + 1, j + 1).dot(prmb[s][1]) if ccf == 0: # Set reference and carry on to next permutation - l_r[c] = PM[prmb[s][0].dot(range(n_f)),:][:,prmb[s][1].dot(range(n_v))][l,c] + l_r[c] = PM[prmb[s][0].dot(range(n_f)), :][ + :, prmb[s][1].dot(range(n_v)) + ][l, c] ccf = 1 else: - d1 = PM[prmb[s][0].dot(range(n_f)),:][:,prmb[s][1].dot(range(n_v))][l,c] + d1 = PM[prmb[s][0].dot(range(n_f)), :][ + :, prmb[s][1].dot(range(n_v)) + ][l, c] d = d1 - l_r[c] if d < 0: n_p -= 1 @@ -1888,35 +1970,35 @@ def PGE(n, u, v): n_p = s + 1 n_s = k + 1 # Update permutations - if n_s-1 > k: - prm[k] = copy.copy(prm[n_s-1]) + if n_s - 1 > k: + prm[k] = copy.copy(prm[n_s - 1]) n_s -= 1 for s in range(n_p): prm[n_s] = copy.copy(prmb[s]) n_s += 1 cf = n_s - prm = {k:prm[k] for k in prm if k < n_s} + prm = {k: prm[k] for k in prm if k < n_s} # If the automorphisms are not already completely restricted, # update them - if S != list(range(1, n_v+1)): + if S != list(range(1, n_v + 1)): # Take the old automorphisms and update by # the restrictions the last worked out # row imposes. c = 0 - M = PM[prm[0][0].dot(range(n_f)),:][:,prm[0][1].dot(range(n_v))][l] + M = PM[prm[0][0].dot(range(n_f)), :][:, prm[0][1].dot(range(n_v))][l] while c < n_v: s = S[c] + 1 S[c] = c + 1 c += 1 - while c < s-1: - if M[c] == M[c-1]: - S[c] = S[c-1] - S[S[c]-1] += 1 + while c < s - 1: + if M[c] == M[c - 1]: + S[c] = S[c - 1] + S[S[c] - 1] += 1 else: S[c] = c + 1 c += 1 # Now we have the perms, we construct PM_max using one of them - PM_max = PM[prm[0][0].dot(range(n_f)),:][:,prm[0][1].dot(range(n_v))] + PM_max = PM[prm[0][0].dot(range(n_f)), :][:, prm[0][1].dot(range(n_v))] # Perform a translation if necessary if affine_transform: v0 = copy.copy(V[0]) @@ -1928,26 +2010,32 @@ def PGE(n, u, v): S_max = [sum([PM_max[i][j] for i in range(n_f)]) for j in range(n_v)] for i in range(n_v): k = i - for j in range(i+1, n_v): - if M_max[j] < M_max[k] or (M_max[j] == M_max[k] and S_max[j] < S_max[k]): + for j in range(i + 1, n_v): + if M_max[j] < M_max[k] or ( + M_max[j] == M_max[k] and S_max[j] < S_max[k] + ): k = j if not k == i: M_max[i], M_max[k] = M_max[k], M_max[i] S_max[i], S_max[k] = S_max[k], S_max[i] - p_c = PGE(n_v, 1+i, 1+k).dot(p_c) + p_c = PGE(n_v, 1 + i, 1 + k).dot(p_c) # Create array of possible NFs. prm = [p_c.dot(l[1]) for l in prm.values()] - Vs = [np.array(fmpz_mat(V.T[:,sig.dot(range(n_v))].tolist()).hnf().tolist(), dtype=int).tolist() for sig in prm] + Vs = [ + np.array( + fmpz_mat(V.T[:, sig.dot(range(n_v))].tolist()).hnf().tolist(), + dtype=int, + ).tolist() + for sig in prm + ] Vmin = min(Vs) if affine_transform: - self._normal_form[args_id] = np.array(Vmin).T[:,:self.dim()] + self._normal_form[args_id] = np.array(Vmin).T[:, : self.dim()] else: self._normal_form[args_id] = np.array(Vmin).T return np.array(self._normal_form[args_id]) - def is_linearly_equivalent(self, - other: "Polytope", - backend: str="palp") -> bool: + def is_linearly_equivalent(self, other: "Polytope", backend: str = "palp") -> bool: """ **Description:** Returns True if the polytopes can be transformed into each other by an @@ -1971,16 +2059,16 @@ def is_linearly_equivalent(self, # True ``` """ - our_normal_form = self.normal_form(affine_transform=False,\ - backend=backend).tolist() - other_normal_form = other.normal_form(affine_transform=False,\ - backend=backend).tolist() + our_normal_form = self.normal_form( + affine_transform=False, backend=backend + ).tolist() + other_normal_form = other.normal_form( + affine_transform=False, backend=backend + ).tolist() - return(our_normal_form == other_normal_form) + return our_normal_form == other_normal_form - def is_affinely_equivalent(self, - other: "Polytope", - backend: str="palp") -> bool: + def is_affinely_equivalent(self, other: "Polytope", backend: str = "palp") -> bool: """ **Description:** Returns True if the polytopes can be transformed into each other by an @@ -2004,17 +2092,20 @@ def is_affinely_equivalent(self, # True ``` """ - our_normal_form = self.normal_form(affine_transform=True,\ - backend=backend).tolist() - other_normal_form = other.normal_form(affine_transform=True,\ - backend=backend).tolist() + our_normal_form = self.normal_form( + affine_transform=True, backend=backend + ).tolist() + other_normal_form = other.normal_form( + affine_transform=True, backend=backend + ).tolist() - return(our_normal_form == other_normal_form) + return our_normal_form == other_normal_form # triangulating # ============= - def _triang_labels(self, - include_points_interior_to_facets:bool=None) -> tuple[int]: + def _triang_labels( + self, include_points_interior_to_facets: bool = None + ) -> tuple[int]: """ **Description:** Constructs the list of point labels of the points that will be used in @@ -2062,16 +2153,18 @@ def _triang_labels(self, else: return self.labels_not_facet - def triangulate(self, - include_points_interior_to_facets: bool = None, - points: "ArrayLike" = None, - make_star: bool = None, - simplices: "ArrayLike" = None, - check_input_simplices: bool=True, - heights: "ArrayLike" = None, - check_heights: bool = True, - backend: str = "cgal", - verbosity: int = 1) -> Triangulation: + def triangulate( + self, + include_points_interior_to_facets: bool = None, + points: "ArrayLike" = None, + make_star: bool = None, + simplices: "ArrayLike" = None, + check_input_simplices: bool = True, + heights: "ArrayLike" = None, + check_heights: bool = True, + backend: str = "cgal", + verbosity: int = 1, + ) -> Triangulation: """ **Description:** Returns a single regular triangulation of the polytope. @@ -2157,16 +2250,20 @@ def triangulate(self, # index mismatch... Raise error if len(simps_inds) > len(points): - error_msg = f"Simplices spanned {simps_inds}, which is "+\ - f"longer than length of points, {points}. " +\ - "Check include_points_interior_to_facets... it "+\ - f"was set to {include_points_interior_to_facets}" + error_msg = ( + f"Simplices spanned {simps_inds}, which is " + + f"longer than length of points, {points}. " + + "Check include_points_interior_to_facets... it " + + f"was set to {include_points_interior_to_facets}" + ) if include_points_interior_to_facets is None: - error_msg += f" and then to {use_pts_in_facets} b/c " +\ - "the polytope is " +\ - ("" if self.is_reflexive() else "not ") +\ - "reflexive." + error_msg += ( + f" and then to {use_pts_in_facets} b/c " + + "the polytope is " + + ("" if self.is_reflexive() else "not ") + + "reflexive." + ) raise ValueError(error_msg) # if heights are provided for all points, trim them @@ -2187,28 +2284,32 @@ def triangulate(self, make_star = False # return triangulation - return Triangulation(self, - points, - make_star=make_star, - simplices=simplices, - check_input_simplices=check_input_simplices, - heights=triang_heights, - check_heights=check_heights, - backend=backend, - verbosity=verbosity) - - def random_triangulations_fast(self, - N: int = None, - c: float = 0.2, - max_retries: int = 500, - make_star: bool = True, - only_fine: bool = True, - include_points_interior_to_facets:bool=None, - points: ArrayLike = None, - backend: str = "cgal", - as_list: bool = False, - progress_bar: bool = True, - seed: int = None) -> "generator | list": + return Triangulation( + self, + points, + make_star=make_star, + simplices=simplices, + check_input_simplices=check_input_simplices, + heights=triang_heights, + check_heights=check_heights, + backend=backend, + verbosity=verbosity, + ) + + def random_triangulations_fast( + self, + N: int = None, + c: float = 0.2, + max_retries: int = 500, + make_star: bool = True, + only_fine: bool = True, + include_points_interior_to_facets: bool = None, + points: ArrayLike = None, + backend: str = "cgal", + as_list: bool = False, + progress_bar: bool = True, + seed: int = None, + ) -> "generator | list": """ **Description:** Constructs pseudorandom regular (optionally fine and star) @@ -2275,13 +2376,14 @@ def random_triangulations_fast(self, rand_triangs = p.random_triangulations_fast(N=10, as_list=True) # Produces the list of 10 triangulations very quickly ``` """ - #if self.ambient_dim() > self.dim(): + # if self.ambient_dim() > self.dim(): # raise NotImplementedError("Only triangulations of " # "full-dimensional polytopes are " # "supported.") if (N is None) and as_list: - raise ValueError("Number of triangulations must be specified when " - "returning a list.") + raise ValueError( + "Number of triangulations must be specified when " "returning a list." + ) if points is not None: points = tuple(sorted(set(points))) @@ -2292,9 +2394,17 @@ def random_triangulations_fast(self, make_star = self.is_reflexive() if self._label_origin not in points: make_star = False - g = random_triangulations_fast_generator(self, points, N=N, c=c, - max_retries=max_retries, make_star=make_star, - only_fine=only_fine, backend=backend, seed=seed) + g = random_triangulations_fast_generator( + self, + points, + N=N, + c=c, + max_retries=max_retries, + make_star=make_star, + only_fine=only_fine, + backend=backend, + seed=seed, + ) if not as_list: return g if progress_bar: @@ -2304,29 +2414,31 @@ def random_triangulations_fast(self, try: triangs_list.append(next(g)) if progress_bar: - pbar.update(len(triangs_list)-pbar.n) + pbar.update(len(triangs_list) - pbar.n) except StopIteration: if progress_bar: - pbar.update(N-pbar.n) + pbar.update(N - pbar.n) break return triangs_list - def random_triangulations_fair(self, - N: int = None, - n_walk: int = None, - n_flip: int = None, - initial_walk_steps: int = None, - walk_step_size: float = 1e-2, - max_steps_to_wall: int = 25, - fine_tune_steps: int = 8, - max_retries: int = 50, - make_star: bool = None, - include_points_interior_to_facets:bool=None, - points: ArrayLike = None, - backend: str = "cgal", - as_list: bool = False, - progress_bar: bool = True, - seed: int = None) -> "generator | list": + def random_triangulations_fair( + self, + N: int = None, + n_walk: int = None, + n_flip: int = None, + initial_walk_steps: int = None, + walk_step_size: float = 1e-2, + max_steps_to_wall: int = 25, + fine_tune_steps: int = 8, + max_retries: int = 50, + make_star: bool = None, + include_points_interior_to_facets: bool = None, + points: ArrayLike = None, + backend: str = "cgal", + as_list: bool = False, + progress_bar: bool = True, + seed: int = None, + ) -> "generator | list": """ **Description:** Constructs pseudorandom regular (optionally star) triangulations of a @@ -2424,32 +2536,42 @@ def random_triangulations_fair(self, desired balance between speed and fairness of the sampling. """ if self.ambient_dim() > self.dim(): - raise NotImplementedError("Only triangulations of full-dimensional polytopes" - "are supported.") + raise NotImplementedError( + "Only triangulations of full-dimensional polytopes" "are supported." + ) if N is None and as_list: - raise ValueError("Number of triangulations must be specified when " - "returning a list.") + raise ValueError( + "Number of triangulations must be specified when " "returning a list." + ) if points is not None: points = tuple(sorted(set(points))) else: points = self._triang_labels(include_points_interior_to_facets) if make_star is None: - make_star = self.is_reflexive() + make_star = self.is_reflexive() if self._label_origin not in points: make_star = False if n_walk is None: - n_walk = len(self.points())//10 + 10 + n_walk = len(self.points()) // 10 + 10 if n_flip is None: - n_flip = len(self.points())//10 + 10 + n_flip = len(self.points()) // 10 + 10 if initial_walk_steps is None: - initial_walk_steps = 2*len(self.points())//10 + 10 + initial_walk_steps = 2 * len(self.points()) // 10 + 10 g = random_triangulations_fair_generator( - self, points, N=N, n_walk=n_walk, n_flip=n_flip, - initial_walk_steps=initial_walk_steps, - walk_step_size=walk_step_size, - max_steps_to_wall=max_steps_to_wall, - fine_tune_steps=fine_tune_steps, max_retries=max_retries, - make_star=make_star, backend=backend, seed=seed) + self, + points, + N=N, + n_walk=n_walk, + n_flip=n_flip, + initial_walk_steps=initial_walk_steps, + walk_step_size=walk_step_size, + max_steps_to_wall=max_steps_to_wall, + fine_tune_steps=fine_tune_steps, + max_retries=max_retries, + make_star=make_star, + backend=backend, + seed=seed, + ) if not as_list: return g if progress_bar: @@ -2459,23 +2581,25 @@ def random_triangulations_fair(self, try: triangs_list.append(next(g)) if progress_bar: - pbar.update(len(triangs_list)-pbar.n) + pbar.update(len(triangs_list) - pbar.n) except StopIteration: if progress_bar: - pbar.update(N-pbar.n) + pbar.update(N - pbar.n) break return triangs_list - def all_triangulations(self, - points: ArrayLike = None, - only_fine: bool = True, - only_regular: bool = True, - only_star: bool = None, - star_origin: int = None, - include_points_interior_to_facets: bool = None, - backend: str = None, - as_list: bool = False, - raw_output: bool = False) -> "generator | list": + def all_triangulations( + self, + points: ArrayLike = None, + only_fine: bool = True, + only_regular: bool = True, + only_star: bool = None, + star_origin: int = None, + include_points_interior_to_facets: bool = None, + backend: str = None, + as_list: bool = False, + raw_output: bool = False, + ) -> "generator | list": """ **Description:** Computes all triangulations of the polytope using TOPCOM. There is the @@ -2535,49 +2659,57 @@ def all_triangulations(self, # 6 ``` """ - if len(self.points()) == self.dim()+1: + if len(self.points()) == self.dim() + 1: # simplex... trivial triangs = None if raw_output: - triangs = [self.points(as_indices=True)[None,:]] + triangs = [self.points(as_indices=True)[None, :]] else: triangs = [Triangulation(self, self.labels)] if as_list: return triangs else: + def gen(): for triang in triangs: - yield(triang) + yield (triang) return gen() - + if only_star is None: only_star = self.is_reflexive() if only_star and star_origin is None: if self.is_reflexive(): star_origin = self._label_origin else: - raise ValueError("The star_origin parameter must be specified " - "when finding star triangulations of " - "non-reflexive polytopes.") + raise ValueError( + "The star_origin parameter must be specified " + "when finding star triangulations of " + "non-reflexive polytopes." + ) if points is not None: points = tuple(sorted(set(points))) else: points = self._triang_labels(include_points_interior_to_facets) if len(points) >= 17: - warnings.warn("Polytopes with more than around 17 points usually " - "have too many triangulations, so this function may " - "take too long or run out of memory.") - - triangs = all_triangulations(self, points, - only_fine=only_fine, - only_regular=only_regular, - only_star=only_star, - star_origin=star_origin, - backend=backend, - raw_output=raw_output) + warnings.warn( + "Polytopes with more than around 17 points usually " + "have too many triangulations, so this function may " + "take too long or run out of memory." + ) + + triangs = all_triangulations( + self, + points, + only_fine=only_fine, + only_regular=only_regular, + only_star=only_star, + star_origin=star_origin, + backend=backend, + raw_output=raw_output, + ) if as_list: return list(triangs) return triangs @@ -2624,55 +2756,61 @@ def hpq(self, p: int, q: int, lattice: str) -> int: """ # check that we support hodge-number calculations for this polytope d = self.dim() - if not self.is_reflexive() or d not in (2,3,4,5): - raise ValueError("Only reflexive polytopes of dimension 2-5 are " - "currently supported.") - + if not self.is_reflexive() or d not in (2, 3, 4, 5): + raise ValueError( + "Only reflexive polytopes of dimension 2-5 are " "currently supported." + ) + # check lattice/configure p accordingly if lattice == "M": - p = d-p-1 + p = d - p - 1 elif lattice != "N": - raise ValueError("Lattice must be specified. " - "Options are: \"N\" or \"M\".") + raise ValueError("Lattice must be specified. " 'Options are: "N" or "M".') # assume p,q ordered such that q>p if p > q: - p,q = q,p + p, q = q, p # easy answers - if (p > d-1) or (q > d-1) or (p < 0) or (q < 0) or (p+q > d-1): + if (p > d - 1) or (q > d - 1) or (p < 0) or (q < 0) or (p + q > d - 1): return 0 - elif (p in (0,d-1)) or (q in (0,d-1)): - if (p == q) or ((p,q) == (0,d-1)): + elif (p in (0, d - 1)) or (q in (0, d - 1)): + if (p == q) or ((p, q) == (0, d - 1)): return 1 return 0 # - if p >= d//2: + if p >= d // 2: tmp_p = p - p = d-q-1 - q = d-tmp_p-1 + p = d - q - 1 + q = d - tmp_p - 1 # calculate hpq hpq = 0 if p == 1: - for f in self.faces(d-q-1): - hpq += len(f.interior_points())*len(f.dual().interior_points()) + for f in self.faces(d - q - 1): + hpq += len(f.interior_points()) * len(f.dual().interior_points()) if q == 1: hpq += len(self.points_not_interior_to_facets()) - d - 1 - if q == d-2: + if q == d - 2: hpq += len(self.dual().points_not_interior_to_facets()) - d - 1 return hpq elif p == 2: - hpq = 44 + 4*self.h11(lattice="N") - 2*self.h12(lattice="N") +\ - 4*self.h13(lattice="N") + hpq = ( + 44 + + 4 * self.h11(lattice="N") + - 2 * self.h12(lattice="N") + + 4 * self.h13(lattice="N") + ) return hpq raise RuntimeError("Error computing Hodge numbers.") - h11 = lambda self,lattice: self.hpq(1,1,lattice=lattice) - h12 = lambda self,lattice: self.hpq(1,2,lattice=lattice); h21 = h12 - h13 = lambda self,lattice: self.hpq(1,3,lattice=lattice); h31 = h13 - h22 = lambda self,lattice: self.hpq(2,2,lattice=lattice) + h11 = lambda self, lattice: self.hpq(1, 1, lattice=lattice) + h12 = lambda self, lattice: self.hpq(1, 2, lattice=lattice) + h21 = h12 + h13 = lambda self, lattice: self.hpq(1, 3, lattice=lattice) + h31 = h13 + h22 = lambda self, lattice: self.hpq(2, 2, lattice=lattice) def chi(self, lattice: str) -> int: """ @@ -2705,14 +2843,14 @@ def chi(self, lattice: str) -> int: ``` """ # check that we support hodge-number calculations for this polytope - if not self.is_reflexive() or self.dim() not in (2,3,4,5): - raise ValueError("Only reflexive polytopes of dimension 2-5 are " - "currently supported.") + if not self.is_reflexive() or self.dim() not in (2, 3, 4, 5): + raise ValueError( + "Only reflexive polytopes of dimension 2-5 are " "currently supported." + ) # input checking - if lattice not in ("N","M"): - raise ValueError("Lattice must be specified. " - "Options are: \"N\" or \"M\".") + if lattice not in ("N", "M"): + raise ValueError("Lattice must be specified. " 'Options are: "N" or "M".') # punt the answer if lattice "M" if lattice == "M": @@ -2728,10 +2866,13 @@ def chi(self, lattice: str) -> int: elif self.dim() == 3: self._chi = self.h11(lattice=lattice) + 4 elif self.dim() == 4: - self._chi = 2*(self.h11(lattice=lattice)-self.h21(lattice=lattice)) + self._chi = 2 * (self.h11(lattice=lattice) - self.h21(lattice=lattice)) elif self.dim() == 5: - self._chi = 48 + 6*(self.h11(lattice=lattice) -\ - self.h12(lattice=lattice) + self.h13(lattice=lattice)) + self._chi = 48 + 6 * ( + self.h11(lattice=lattice) + - self.h12(lattice=lattice) + + self.h13(lattice=lattice) + ) # return return self._chi @@ -2765,24 +2906,27 @@ def is_favorable(self, lattice: str) -> bool: # False ``` """ - if lattice=="N": + if lattice == "N": if self._is_favorable is None: - self._is_favorable = (len(self.points_not_interior_to_facets()) - == self.h11(lattice="N")+self.dim()+1) + self._is_favorable = ( + len(self.points_not_interior_to_facets()) + == self.h11(lattice="N") + self.dim() + 1 + ) return self._is_favorable - elif lattice=='M': + elif lattice == "M": return self.dual().is_favorable(lattice="N") - raise ValueError("Lattice must be specified. " - "Options are: \"N\" or \"M\".") + raise ValueError("Lattice must be specified. " 'Options are: "N" or "M".') # glsm # ==== - def glsm_charge_matrix(self, - include_origin: bool = True, - include_points_interior_to_facets: bool = False, - points: ArrayLike = None, - integral: bool = True) -> np.ndarray: + def glsm_charge_matrix( + self, + include_origin: bool = True, + include_points_interior_to_facets: bool = False, + points: ArrayLike = None, + integral: bool = True, + ) -> np.ndarray: """ **Description:** Computes the GLSM charge matrix of the theory resulting from this @@ -2830,14 +2974,16 @@ def glsm_charge_matrix(self, """ # check that this makes sense if not self.is_reflexive(): - raise ValueError("The GLSM charge matrix can only be computed for " - "reflexive polytopes.") + raise ValueError( + "The GLSM charge matrix can only be computed for " + "reflexive polytopes." + ) # Set up the list of points that will be used. if points is not None: # We always add the origin, but remove it later if necessary - pts_ind = set(list(points)+[0]) - if (min(pts_ind)<0) or (max(pts_ind)>self.points().shape[0]): + pts_ind = set(list(points) + [0]) + if (min(pts_ind) < 0) or (max(pts_ind) > self.points().shape[0]): raise ValueError("An index is out of the allowed range.") include_origin = 0 in points @@ -2848,11 +2994,11 @@ def glsm_charge_matrix(self, pts_ind = tuple(pts_ind) # check if we know the answer - if (pts_ind,integral) in self._glsm_charge_matrix: - out = np.array(self._glsm_charge_matrix[(pts_ind,integral)]) + if (pts_ind, integral) in self._glsm_charge_matrix: + out = np.array(self._glsm_charge_matrix[(pts_ind, integral)]) if (not include_origin) and (points is None): - return out[:,1:] + return out[:, 1:] else: return out @@ -2861,38 +3007,49 @@ def glsm_charge_matrix(self, # find a basis of columns if integral: linrel = self.points()[list(pts_ind)].T - sublat_ind = int(round(np.linalg.det(np.array(fmpz_mat(linrel.tolist()).snf().tolist(), dtype=int)[:,:linrel.shape[0]]))) - norms = [np.linalg.norm(p,1) for p in linrel.T] + sublat_ind = int( + round( + np.linalg.det( + np.array(fmpz_mat(linrel.tolist()).snf().tolist(), dtype=int)[ + :, : linrel.shape[0] + ] + ) + ) + ) + norms = [np.linalg.norm(p, 1) for p in linrel.T] linrel = np.insert(linrel, 0, np.ones(linrel.shape[1], dtype=int), axis=0) good_exclusions = 0 basis_exc = [] indices = np.argsort(norms) - indices[:linrel.shape[0]] = np.sort(indices[:linrel.shape[0]]) + indices[: linrel.shape[0]] = np.sort(indices[: linrel.shape[0]]) for n_try in range(14): if n_try == 1: indices[:] = np.array(range(linrel.shape[1])) elif n_try == 2: - pts_lll = np.array(fmpz_mat(linrel[1:,:].tolist()).lll().tolist(), dtype=int) - norms = [np.linalg.norm(p,1) for p in pts_lll.T] + pts_lll = np.array( + fmpz_mat(linrel[1:, :].tolist()).lll().tolist(), + dtype=int, + ) + norms = [np.linalg.norm(p, 1) for p in pts_lll.T] indices = np.argsort(norms) - indices[:linrel.shape[0]] = np.sort(indices[:linrel.shape[0]]) + indices[: linrel.shape[0]] = np.sort(indices[: linrel.shape[0]]) elif n_try == 3: - indices[:] = np.array([0] + list(range(1,linrel.shape[1]))[::-1]) - indices[:linrel.shape[0]] = np.sort(indices[:linrel.shape[0]]) + indices[:] = np.array([0] + list(range(1, linrel.shape[1]))[::-1]) + indices[: linrel.shape[0]] = np.sort(indices[: linrel.shape[0]]) elif n_try > 3: if n_try == 4: np.random.seed(1337) np.random.shuffle(indices[1:]) - indices[:linrel.shape[0]] = np.sort(indices[:linrel.shape[0]]) + indices[: linrel.shape[0]] = np.sort(indices[: linrel.shape[0]]) - for ctr in range(np.prod(linrel.shape)+1): - found_good_basis=True + for ctr in range(np.prod(linrel.shape) + 1): + found_good_basis = True ctr += 1 if ctr > 0: - st = max([good_exclusions,1]) + st = max([good_exclusions, 1]) indices[st:] = np.roll(indices[st:], -1) - indices[:linrel.shape[0]] = np.sort(indices[:linrel.shape[0]]) - linrel_rand = np.array(linrel[:,indices]) + indices[: linrel.shape[0]] = np.sort(indices[: linrel.shape[0]]) + linrel_rand = np.array(linrel[:, indices]) try: linrel_hnf = fmpz_mat(linrel_rand.tolist()).hnf() except: @@ -2902,13 +3059,13 @@ def glsm_charge_matrix(self, basis_exc = [] tmp_sublat_ind = 1 for v in linrel_rand: - for i,ii in enumerate(v): - if ii==0: + for i, ii in enumerate(v): + if ii == 0: continue tmp_sublat_ind *= abs(ii) if sublat_ind % tmp_sublat_ind == 0: - v *= ii//abs(ii) + v *= ii // abs(ii) good_exclusions += 1 else: found_good_basis = False @@ -2922,102 +3079,133 @@ def glsm_charge_matrix(self, break if not found_good_basis: - warnings.warn("An integral basis could not be found. " - "A non-integral one will be computed. However, " - "this will not be usable as a basis of divisors " - "for the ToricVariety or CalabiYau classes.") - if pts_ind == tuple(self.points_not_interior_to_facets(as_indices=True)): - warnings.warn("Please let the developers know about the " - "polytope that caused this issue. " - "Here are the vertices of the polytope: " - f"{self.vertices().tolist()}") - return self.glsm_charge_matrix(include_origin=include_origin, - include_points_interior_to_facets=include_points_interior_to_facets, - points=points, integral=False) - linrel_dict = {ii:i for i,ii in enumerate(indices)} - linrel = np.array(linrel_rand[:,[linrel_dict[i] for i in range(linrel_rand.shape[1])]]) - basis_ind = np.array([i for i in range(linrel.shape[1]) if linrel_dict[i] not in basis_exc], dtype=int) + warnings.warn( + "An integral basis could not be found. " + "A non-integral one will be computed. However, " + "this will not be usable as a basis of divisors " + "for the ToricVariety or CalabiYau classes." + ) + if pts_ind == tuple( + self.points_not_interior_to_facets(as_indices=True) + ): + warnings.warn( + "Please let the developers know about the " + "polytope that caused this issue. " + "Here are the vertices of the polytope: " + f"{self.vertices().tolist()}" + ) + return self.glsm_charge_matrix( + include_origin=include_origin, + include_points_interior_to_facets=include_points_interior_to_facets, + points=points, + integral=False, + ) + linrel_dict = {ii: i for i, ii in enumerate(indices)} + linrel = np.array( + linrel_rand[:, [linrel_dict[i] for i in range(linrel_rand.shape[1])]] + ) + basis_ind = np.array( + [i for i in range(linrel.shape[1]) if linrel_dict[i] not in basis_exc], + dtype=int, + ) basis_exc = np.array([indices[i] for i in basis_exc]) - glsm = np.zeros((linrel.shape[1]-linrel.shape[0],linrel.shape[1]), dtype=int) - glsm[:,basis_ind] = np.eye(len(basis_ind), dtype=int) + glsm = np.zeros( + (linrel.shape[1] - linrel.shape[0], linrel.shape[1]), dtype=int + ) + glsm[:, basis_ind] = np.eye(len(basis_ind), dtype=int) for nb in basis_exc[::-1]: - tup = [(k,kk) for k,kk in enumerate(linrel[:,nb]) if kk] + tup = [(k, kk) for k, kk in enumerate(linrel[:, nb]) if kk] if sublat_ind % tup[-1][1] != 0: raise RuntimeError("Problem with linear relations") - i,ii = tup[-1] + i, ii = tup[-1] if integral: - glsm[:,nb] = -glsm.dot(linrel[i])//ii + glsm[:, nb] = -glsm.dot(linrel[i]) // ii else: - glsm[i,:] *= ii - glsm[:,nb] = -glsm.dot(linrel[i]) - else: # Non-integral basis - pts = self.points()[list(pts_ind)[1:]] # Exclude the origin - pts_norms = [np.linalg.norm(p,1) for p in pts] + glsm[i, :] *= ii + glsm[:, nb] = -glsm.dot(linrel[i]) + else: # Non-integral basis + pts = self.points()[list(pts_ind)[1:]] # Exclude the origin + pts_norms = [np.linalg.norm(p, 1) for p in pts] pts_order = np.argsort(pts_norms) # Find good lattice basis good_lattice_basis = pts_order[:1] current_rank = 1 for p in pts_order: tmp = pts[np.append(good_lattice_basis, p)] - rank = np.linalg.matrix_rank(np.dot(tmp.T,tmp)) - if rank>current_rank: + rank = np.linalg.matrix_rank(np.dot(tmp.T, tmp)) + if rank > current_rank: good_lattice_basis = np.append(good_lattice_basis, p) current_rank = rank - if rank==self.dim(): + if rank == self.dim(): break good_lattice_basis = np.sort(good_lattice_basis) glsm_basis = [i for i in range(len(pts)) if i not in good_lattice_basis] M = fmpq_mat(pts[good_lattice_basis].T.tolist()) M_inv = np.array(M.inv().tolist()) - extra_pts = -1*np.dot(M_inv,pts[glsm_basis].T) - row_scalings = np.array([np.lcm.reduce([int(ii.q) for ii in i]) for i in extra_pts]) - column_scalings = np.array([np.lcm.reduce([int(ii.q) for ii in i]) for i in extra_pts.T]) + extra_pts = -1 * np.dot(M_inv, pts[glsm_basis].T) + row_scalings = np.array( + [np.lcm.reduce([int(ii.q) for ii in i]) for i in extra_pts] + ) + column_scalings = np.array( + [np.lcm.reduce([int(ii.q) for ii in i]) for i in extra_pts.T] + ) extra_rows = np.multiply(extra_pts, row_scalings[:, None]) extra_rows = np.array([[int(ii.p) for ii in i] for i in extra_rows]) extra_columns = np.multiply(extra_pts.T, column_scalings[:, None]).T extra_columns = np.array([[int(ii.p) for ii in i] for i in extra_columns]) glsm = np.diag(column_scalings) - for p,pp in enumerate(good_lattice_basis): + for p, pp in enumerate(good_lattice_basis): glsm = np.insert(glsm, pp, extra_columns[p], axis=1) - origin_column = -np.dot(glsm,np.ones(len(glsm[0]))) + origin_column = -np.dot(glsm, np.ones(len(glsm[0]))) glsm = np.insert(glsm, 0, origin_column, axis=1) linear_relations = extra_rows - extra_linear_relation_columns = -1*np.diag(row_scalings) - for p,pp in enumerate(good_lattice_basis): - linear_relations = np.insert(linear_relations, pp, extra_linear_relation_columns[p], axis=1) + extra_linear_relation_columns = -1 * np.diag(row_scalings) + for p, pp in enumerate(good_lattice_basis): + linear_relations = np.insert( + linear_relations, + pp, + extra_linear_relation_columns[p], + axis=1, + ) linear_relations = np.insert(linear_relations, 0, np.ones(len(pts)), axis=0) - linear_relations = np.insert(linear_relations, 0, np.zeros(self.dim()+1), axis=1) + linear_relations = np.insert( + linear_relations, 0, np.zeros(self.dim() + 1), axis=1 + ) linear_relations[0][0] = 1 linrel = linear_relations basis_ind = glsm_basis # check that everything was computed correctly - if (np.linalg.matrix_rank(glsm[:,basis_ind]) != len(basis_ind) - or any(glsm.dot(linrel.T).flat) - or any(glsm.dot(self.points()[list(pts_ind)]).flat)): + if ( + np.linalg.matrix_rank(glsm[:, basis_ind]) != len(basis_ind) + or any(glsm.dot(linrel.T).flat) + or any(glsm.dot(self.points()[list(pts_ind)]).flat) + ): raise RuntimeError("Error finding basis") # cache the results if integral: - self._glsm_charge_matrix[(pts_ind,integral)] = glsm - self._glsm_linrels[(pts_ind,integral)] = linrel - self._glsm_basis[(pts_ind,integral)] = basis_ind + self._glsm_charge_matrix[(pts_ind, integral)] = glsm + self._glsm_linrels[(pts_ind, integral)] = linrel + self._glsm_basis[(pts_ind, integral)] = basis_ind - self._glsm_charge_matrix[(pts_ind,False)] = glsm - self._glsm_linrels[(pts_ind,False)] = linrel - self._glsm_basis[(pts_ind,False)] = basis_ind + self._glsm_charge_matrix[(pts_ind, False)] = glsm + self._glsm_linrels[(pts_ind, False)] = linrel + self._glsm_basis[(pts_ind, False)] = basis_ind # return if (not include_origin) and (points is None): - return np.array(self._glsm_charge_matrix[(pts_ind,integral)][:,1:]) + return np.array(self._glsm_charge_matrix[(pts_ind, integral)][:, 1:]) else: - return np.array(self._glsm_charge_matrix[(pts_ind,integral)]) + return np.array(self._glsm_charge_matrix[(pts_ind, integral)]) - def glsm_linear_relations(self, - include_origin: bool = True, - include_points_interior_to_facets: bool = False, - points: ArrayLike = None, - integral: bool = True) -> np.ndarray: + def glsm_linear_relations( + self, + include_origin: bool = True, + include_points_interior_to_facets: bool = False, + points: ArrayLike = None, + integral: bool = True, + ) -> np.ndarray: """ **Description:** Computes the linear relations of the GLSM charge matrix. @@ -3071,7 +3259,7 @@ def glsm_linear_relations(self, ``` """ if points is not None: - pts_ind = tuple(set(list(points)+[0])) + pts_ind = tuple(set(list(points) + [0])) if min(pts_ind) < 0 or max(pts_ind) > self.points().shape[0]: raise ValueError("An index is out of the allowed range.") include_origin = 0 in points @@ -3079,24 +3267,29 @@ def glsm_linear_relations(self, pts_ind = tuple(range(self.points().shape[0])) else: pts_ind = tuple(range(self.points_not_interior_to_facets().shape[0])) - if (pts_ind,integral) in self._glsm_linrels: + if (pts_ind, integral) in self._glsm_linrels: if not include_origin and points is None: - return np.array(self._glsm_linrels[(pts_ind,integral)][1:,1:]) - return np.array(self._glsm_linrels[(pts_ind,integral)]) + return np.array(self._glsm_linrels[(pts_ind, integral)][1:, 1:]) + return np.array(self._glsm_linrels[(pts_ind, integral)]) # If linear relations are not cached we just call the GLSM charge # matrix function since they are computed there - self.glsm_charge_matrix(include_origin=True, - include_points_interior_to_facets=include_points_interior_to_facets, - points=points, integral=integral) + self.glsm_charge_matrix( + include_origin=True, + include_points_interior_to_facets=include_points_interior_to_facets, + points=points, + integral=integral, + ) if not include_origin and points is None: - return np.array(self._glsm_linrels[(pts_ind,integral)][1:,1:]) - return np.array(self._glsm_linrels[(pts_ind,integral)]) + return np.array(self._glsm_linrels[(pts_ind, integral)][1:, 1:]) + return np.array(self._glsm_linrels[(pts_ind, integral)]) - def glsm_basis(self, - include_origin: bool = True, - include_points_interior_to_facets: bool = False, - points: ArrayLike = None, - integral: bool =True) -> np.ndarray: + def glsm_basis( + self, + include_origin: bool = True, + include_points_interior_to_facets: bool = False, + points: ArrayLike = None, + integral: bool = True, + ) -> np.ndarray: """ **Description:** Computes a basis of columns of the GLSM charge matrix. @@ -3135,7 +3328,7 @@ def glsm_basis(self, ``` """ if points is not None: - pts_ind = tuple(set(list(points)+[0])) + pts_ind = tuple(set(list(points) + [0])) if min(pts_ind) < 0 or max(pts_ind) > self.points().shape[0]: raise ValueError("An index is out of the allowed range.") include_origin = 0 in points @@ -3143,18 +3336,21 @@ def glsm_basis(self, pts_ind = tuple(range(self.points().shape[0])) else: pts_ind = tuple(range(self.points_not_interior_to_facets().shape[0])) - if (pts_ind,integral) in self._glsm_basis: + if (pts_ind, integral) in self._glsm_basis: if not include_origin and points is None: - return np.array(self._glsm_basis[(pts_ind,integral)]) - 1 - return np.array(self._glsm_basis[(pts_ind,integral)]) + return np.array(self._glsm_basis[(pts_ind, integral)]) - 1 + return np.array(self._glsm_basis[(pts_ind, integral)]) # If basis is not cached we just call the GLSM charge matrix function # since it is computed there - self.glsm_charge_matrix(include_origin=True, - include_points_interior_to_facets=include_points_interior_to_facets, - points=points, integral=integral) + self.glsm_charge_matrix( + include_origin=True, + include_points_interior_to_facets=include_points_interior_to_facets, + points=points, + integral=integral, + ) if not include_origin and points is None: - return np.array(self._glsm_basis[(pts_ind,integral)]) - 1 - return np.array(self._glsm_basis[(pts_ind,integral)]) + return np.array(self._glsm_basis[(pts_ind, integral)]) - 1 + return np.array(self._glsm_basis[(pts_ind, integral)]) # misc # ==== @@ -3178,8 +3374,12 @@ def minkowski_sum(self, other: "Polytope") -> "Polytope": # A 3-dimensional reflexive lattice polytope in ZZ^3 ``` """ - points = set(map(lambda verts: tuple(sum(verts)),\ - itertools.product(self.vertices(), other.vertices()))) + points = set( + map( + lambda verts: tuple(sum(verts)), + itertools.product(self.vertices(), other.vertices()), + ) + ) return Polytope(list(points)) def volume(self) -> int: @@ -3214,12 +3414,13 @@ def volume(self) -> int: if self.dim() == 0: self._volume = 0 elif self.dim() == 1: - self._volume = max(self.points(optimal=True)) \ - - min(self.points(optimal=True)) + self._volume = max(self.points(optimal=True)) - min( + self.points(optimal=True) + ) else: self._volume = ConvexHull(self.points(optimal=True)).volume self._volume *= math.factorial(self.dim()) - self._volume = int(round( self._volume )) + self._volume = int(round(self._volume)) # return return self._volume @@ -3251,46 +3452,50 @@ def find_2d_reflexive_subpolytopes(self) -> list["Polytope"]: dual_vert = self.dual().vertices() # Construct the sets S_i by finding the maximum dot product with dual # vertices - S_i = [[]]*3 + S_i = [[]] * 3 for p in pts: m = max(p.dot(v) for v in dual_vert) - if m in (1,2,3): - S_i[m-1].append(tuple(p)) + if m in (1, 2, 3): + S_i[m - 1].append(tuple(p)) # Check each of the three conditions gen_pts = [] for i in range(len(S_i[0])): if tuple(-np.array(S_i[0][i])) in S_i[0]: - for j in range(i+1,len(S_i[0])): - if (tuple(-np.array(S_i[0][j])) in S_i[0] - and tuple(-np.array(S_i[0][i]))!=S_i[0][j]): - gen_pts.append((S_i[0][i],S_i[0][j])) + for j in range(i + 1, len(S_i[0])): + if ( + tuple(-np.array(S_i[0][j])) in S_i[0] + and tuple(-np.array(S_i[0][i])) != S_i[0][j] + ): + gen_pts.append((S_i[0][i], S_i[0][j])) for i in range(len(S_i[1])): - for j in range(i+1,len(S_i[1])): - p = tuple(-np.array(S_i[1][i])-np.array(S_i[1][j])) + for j in range(i + 1, len(S_i[1])): + p = tuple(-np.array(S_i[1][i]) - np.array(S_i[1][j])) if p in S_i[0] or p in S_i[1]: - gen_pts.append((S_i[1][i],S_i[1][j])) + gen_pts.append((S_i[1][i], S_i[1][j])) for i in range(len(S_i[2])): - for j in range(i+1,len(S_i[2])): - p = -np.array(S_i[2][i])-np.array(S_i[2][j]) - if all(c%2 == 0 for c in p) and tuple(p//2) in S_i[0]: - gen_pts.append((S_i[2][i],S_i[2][j])) + for j in range(i + 1, len(S_i[2])): + p = -np.array(S_i[2][i]) - np.array(S_i[2][j]) + if all(c % 2 == 0 for c in p) and tuple(p // 2) in S_i[0]: + gen_pts.append((S_i[2][i], S_i[2][j])) polys_2d = set() - for p1,p2 in gen_pts: + for p1, p2 in gen_pts: pts_2d = set() for p in pts: - if np.linalg.matrix_rank((p1,p2,p)) == 2: + if np.linalg.matrix_rank((p1, p2, p)) == 2: pts_2d.add(tuple(p)) if np.linalg.matrix_rank(list(pts_2d)) == 2: polys_2d.add(tuple(sorted(pts_2d))) return [Polytope(pp) for pp in polys_2d] - def nef_partitions(self, - keep_symmetric: bool = False, - keep_products: bool = False, - keep_projections: bool = False, - codim: int = 2, - compute_hodge_numbers: bool = True, - return_hodge_numbers: bool = False) -> tuple: + def nef_partitions( + self, + keep_symmetric: bool = False, + keep_products: bool = False, + keep_projections: bool = False, + codim: int = 2, + compute_hodge_numbers: bool = True, + return_hodge_numbers: bool = False, + ) -> tuple: """ **Description:** Computes the nef partitions of the polytope using PALP. @@ -3334,16 +3539,25 @@ def nef_partitions(self, ``` """ if not config._exp_features_enabled: - raise Exception("The experimental features must be enabled to " - "compute nef partitions.") + raise Exception( + "The experimental features must be enabled to " + "compute nef partitions." + ) if return_hodge_numbers: compute_hodge_numbers = True - args_id = (keep_symmetric,keep_products,keep_projections,codim, - compute_hodge_numbers) - if self._nef_parts.get(args_id,None) is not None: - return (self._nef_parts.get(args_id) if return_hodge_numbers - or not compute_hodge_numbers - else self._nef_parts.get(args_id)[0]) + args_id = ( + keep_symmetric, + keep_products, + keep_projections, + codim, + compute_hodge_numbers, + ) + if self._nef_parts.get(args_id, None) is not None: + return ( + self._nef_parts.get(args_id) + if return_hodge_numbers or not compute_hodge_numbers + else self._nef_parts.get(args_id)[0] + ) if not self.is_reflexive(): raise ValueError("The polytope must be reflexive") flags = ("-N", "-V", "-p", f"-c{codim}") @@ -3353,20 +3567,28 @@ def nef_partitions(self, flags += ("-D",) if keep_projections: flags += ("-P",) - palp = subprocess.Popen((config.palp_path + "nef-11d.x",)+flags, - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, universal_newlines=True) + palp = subprocess.Popen( + (config.palp_path + "nef-11d.x",) + flags, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) vert_str = "" vert = [tuple(pt) for pt in self.vertices()] for pt in vert: - vert_str += (str(pt).replace("(","").replace(")","").replace(","," ") + "\n") - palp_out = palp.communicate(input=f"{len(vert)} {self.dim()}\n" + vert_str + "\n")[0] + vert_str += ( + str(pt).replace("(", "").replace(")", "").replace(",", " ") + "\n" + ) + palp_out = palp.communicate( + input=f"{len(vert)} {self.dim()}\n" + vert_str + "\n" + )[0] if "Vertices of P" not in palp_out: raise RuntimeError(f"PALP error. Full output: {palp_out}") palp_out = palp_out.split("\n") n_parts = 0 # Read number of nef partitions and vertices to make sure it looks right - for i,line in enumerate(palp_out): + for i, line in enumerate(palp_out): if "#part" in line: n_parts = int(line.split("=")[-1]) if "Vertices of P" not in line: @@ -3374,28 +3596,39 @@ def nef_partitions(self, pts_shape = [int(c) for c in line.split()[:2]] tmp_pts = np.empty(pts_shape, dtype=int) for j in range(pts_shape[0]): - tmp_pts[j,:] = [int(c) for c in palp_out[i+j+1].split()] - nef_part_start = i+j+2 + tmp_pts[j, :] = [int(c) for c in palp_out[i + j + 1].split()] + nef_part_start = i + j + 2 break - pts_out = (tmp_pts.T if pts_shape[0] < pts_shape[1] else tmp_pts) + pts_out = tmp_pts.T if pts_shape[0] < pts_shape[1] else tmp_pts nef_parts = [] for n in range(n_parts): - if "V" not in palp_out[nef_part_start+n]: + if "V" not in palp_out[nef_part_start + n]: break tmp_partition = [] - for nn in range(codim-1): + for nn in range(codim - 1): tmp_part = [] - start = palp_out[nef_part_start+n].find(f"V{nn if codim>2 else ''}:") - end = palp_out[nef_part_start+n][start:].find(" ")+start - for s in palp_out[nef_part_start+n][start+(2 if codim==2 else 3):end].split(): + start = palp_out[nef_part_start + n].find(f"V{nn if codim>2 else ''}:") + end = palp_out[nef_part_start + n][start:].find(" ") + start + for s in palp_out[nef_part_start + n][ + start + (2 if codim == 2 else 3) : end + ].split(): if "V" in s or "D" in s or "P" in s or "sec" in s: break tmp_part.append(int(s)) tmp_partition.append(tmp_part) - tmp_part = [i for i in range(len(vert)) if not any(i in part for part in tmp_partition)] + tmp_part = [ + i + for i in range(len(vert)) + if not any(i in part for part in tmp_partition) + ] tmp_partition.append(tmp_part) # We have to reindex to match polytope indices - nef_parts.append(tuple(tuple(self.points_to_indices(pts_out[part])) for part in tmp_partition)) + nef_parts.append( + tuple( + tuple(self.points_to_indices(pts_out[part])) + for part in tmp_partition + ) + ) if compute_hodge_numbers: flags = ("-N", "-V", "-H", f"-c{codim}") if keep_symmetric: @@ -3405,26 +3638,36 @@ def nef_partitions(self, if keep_projections: flags += ("-P",) cy_dim = self.dim() - codim - palp = subprocess.Popen((config.palp_path + "nef-11d.x",)+flags, - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, universal_newlines=True) - palp_out = palp.communicate(input=f"{len(vert)} {self.dim()}\n" + vert_str + "\n")[0] + palp = subprocess.Popen( + (config.palp_path + "nef-11d.x",) + flags, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + palp_out = palp.communicate( + input=f"{len(vert)} {self.dim()}\n" + vert_str + "\n" + )[0] data = palp_out.split(f"h {cy_dim} {cy_dim}")[1:] hodge_nums = [] for i in range(len(data)): - hodge_nums.append(tuple(int(h) for h in data[i].split()[:(cy_dim+1)**2])) + hodge_nums.append( + tuple(int(h) for h in data[i].split()[: (cy_dim + 1) ** 2]) + ) if len(hodge_nums) != len(nef_parts): raise RuntimeError("Unexpected length mismatch.") - nef_parts = (tuple(nef_parts),tuple(hodge_nums)) + nef_parts = (tuple(nef_parts), tuple(hodge_nums)) self._nef_parts[args_id] = tuple(nef_parts) - return (self._nef_parts.get(args_id) if return_hodge_numbers - or not compute_hodge_numbers - else self._nef_parts.get(args_id)[0]) + return ( + self._nef_parts.get(args_id) + if return_hodge_numbers or not compute_hodge_numbers + else self._nef_parts.get(args_id)[0] + ) + # utils # ----- -def poly_v_to_h(pts: ArrayLike, - backend: str) -> (ArrayLike, None): +def poly_v_to_h(pts: ArrayLike, backend: str) -> (ArrayLike, None): """ **Description:** Generate the H-representation of a polytope, given the V-representation. @@ -3453,22 +3696,21 @@ def poly_v_to_h(pts: ArrayLike, # insert points to generator system for pt in pts: - ppl_pt = ppl.point(sum(pt[i]*vrs[i] for i in range(dim))) + ppl_pt = ppl.point(sum(pt[i] * vrs[i] for i in range(dim))) gs.insert(ppl_pt) # find polytope, hyperplanes poly = ppl.C_Polyhedron(gs) ineqs = [] for ineq in poly.minimized_constraints(): - ineqs.append(list(ineq.coefficients()) +\ - [ineq.inhomogeneous_term()]) + ineqs.append(list(ineq.coefficients()) + [ineq.inhomogeneous_term()]) ineqs = np.array(ineqs, dtype=int) elif backend == "qhull": if dim == 1: # qhull cannot handle 1-dimensional polytopes poly = None - ineqs = np.array([[1,-min(pts)], [-1,max(pts)]]) + ineqs = np.array([[1, -min(pts)], [-1, max(pts)]]) else: poly = ConvexHull(pts) @@ -3477,7 +3719,7 @@ def poly_v_to_h(pts: ArrayLike, ineqs = set() for eq in poly.equations: g = abs(gcd_list(eq)) - ineqs.add(tuple(-int(round(i/g)) for i in eq)) + ineqs.add(tuple(-int(round(i / g)) for i in eq)) ineqs = np.array(list(ineqs), dtype=int) elif backend == "palp": @@ -3490,17 +3732,19 @@ def poly_v_to_h(pts: ArrayLike, pt_list = "" pts = {tuple(pt) for pt in pts} for pt in pts: - pt_str = str(pt).replace("(","").replace(")","") - pt_str = pt_str.replace(","," ") + pt_str = str(pt).replace("(", "").replace(")", "") + pt_str = pt_str.replace(",", " ") pt_list += pt_str + "\n" palp_in = f"{len(pts)} {dim}\n{pt_list}\n" # execute it! - palp = subprocess.Popen((config.palp_path + "poly.x", "-e"), - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True) + palp = subprocess.Popen( + (config.palp_path + "poly.x", "-e"), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) palp_out = palp.communicate(input=palp_in)[0] # check for errors @@ -3509,14 +3753,16 @@ def poly_v_to_h(pts: ArrayLike, # parse results palp_out = palp_out.split("\n") - for i,line in enumerate(palp_out): + for i, line in enumerate(palp_out): if "Equations" not in line: continue - is_reflexive = ("Vertices" in line) + is_reflexive = "Vertices" in line ineqs_shape = [int(c) for c in line.split()[:2]] - ineqs = [[int(c) for c in palp_out[i+j+1].split()] - for j in range(ineqs_shape[0])] + ineqs = [ + [int(c) for c in palp_out[i + j + 1].split()] + for j in range(ineqs_shape[0]) + ] break # Check if transposed @@ -3526,15 +3772,18 @@ def poly_v_to_h(pts: ArrayLike, ineqs = ineqs.T if is_reflexive: - col_of_1s = np.ones((ineqs.shape[0], 1),dtype=int) - ineqs = np.hstack((ineqs,col_of_1s)) + col_of_1s = np.ones((ineqs.shape[0], 1), dtype=int) + ineqs = np.hstack((ineqs, col_of_1s)) return ineqs, poly -def saturating_lattice_pts(pts_in: [tuple], - ineqs: ArrayLike = None, - dim: int = None, - backend: str = None) -> (ArrayLike, [frozenset]): + +def saturating_lattice_pts( + pts_in: [tuple], + ineqs: ArrayLike = None, + dim: int = None, + backend: str = None, +) -> (ArrayLike, [frozenset]): """ **Description:** Computes the lattice points contained in conv(pts), along with the indices @@ -3552,14 +3801,14 @@ def saturating_lattice_pts(pts_in: [tuple], A list of sets of all inequalities each lattice point saturates. """ # check inputs - if isinstance(pts_in,list) and isinstance(pts_in[0],tuple): + if isinstance(pts_in, list) and isinstance(pts_in[0], tuple): pts = pts_in else: pts = [tuple(pt) for pt in pts_in] # fill in missing inputs if dim is None: - dim = np.linalg.matrix_rank([list(pt)+[1] for pt in pts]) - 1 + dim = np.linalg.matrix_rank([list(pt) + [1] for pt in pts]) - 1 if backend is None: if 1 <= dim <= 4: @@ -3567,7 +3816,7 @@ def saturating_lattice_pts(pts_in: [tuple], else: backend = "palp" - if dim == 0: # 0-dimensional polytopes are finicky + if dim == 0: # 0-dimensional polytopes are finicky backend = "palp" if ineqs is None: @@ -3584,18 +3833,20 @@ def saturating_lattice_pts(pts_in: [tuple], pt_list = "" pts_set = set(pts) for pt in pts_set: - pt_str = str(pt).replace("(","").replace(")","") - pt_str = pt_str.replace(","," ") + pt_str = str(pt).replace("(", "").replace(")", "") + pt_str = pt_str.replace(",", " ") pt_list += pt_str + "\n" palp_in = f"{len(pts_set)} {dim}\n{pt_list}\n" # prep PALP - palp = subprocess.Popen((config.palp_path + "poly.x", "-p"), - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True) - + palp = subprocess.Popen( + (config.palp_path + "poly.x", "-p"), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + # do the work and read output palp_out = palp.communicate(input=palp_in)[0] if "Points of P" not in palp_out: @@ -3603,7 +3854,7 @@ def saturating_lattice_pts(pts_in: [tuple], # parse the outputs palp_out = palp_out.split("\n") - for i,line in enumerate(palp_out): + for i, line in enumerate(palp_out): if "Points of P" not in line: continue @@ -3611,7 +3862,7 @@ def saturating_lattice_pts(pts_in: [tuple], pts_shape = [int(c) for c in line.split()[:2]] tmp_pts = np.empty(pts_shape, dtype=int) for j in range(pts_shape[0]): - tmp_pts[j,:] =[int(c) for c in palp_out[i+j+1].split()] + tmp_pts[j, :] = [int(c) for c in palp_out[i + j + 1].split()] break @@ -3622,8 +3873,12 @@ def saturating_lattice_pts(pts_in: [tuple], pts_all = tmp_pts # find inequialities each point saturates - facet_ind = [frozenset(i for i,ii in enumerate(ineqs) if\ - ii[:-1].dot(pt) + ii[-1] == 0) for pt in pts_all] + facet_ind = [ + frozenset( + i for i, ii in enumerate(ineqs) if ii[:-1].dot(pt) + ii[-1] == 0 + ) + for pt in pts_all + ] # Otherwise use the algorithm by Volker Braun. # This is redistributed under GNU General Public License version 2+. @@ -3641,12 +3896,12 @@ def saturating_lattice_pts(pts_in: [tuple], box_max = box_max[diameter_index] # Construct the inverse permutation - orig_dict = {j:i for i,j in enumerate(diameter_index)} + orig_dict = {j: i for i, j in enumerate(diameter_index)} orig_perm = [orig_dict[i] for i in range(dim)] # Inequalities must also have their coordinates permuted ineqs = ineqs.copy() - ineqs[:,:-1] = ineqs[:,diameter_index] + ineqs[:, :-1] = ineqs[:, diameter_index] # Find all lattice points and apply the inverse permutation pts_all = [] @@ -3654,34 +3909,35 @@ def saturating_lattice_pts(pts_in: [tuple], p = np.array(box_min) while True: - tmp_v = ineqs[:,1:-1].dot(p[1:]) + ineqs[:,-1] + tmp_v = ineqs[:, 1:-1].dot(p[1:]) + ineqs[:, -1] # Find the lower bound for the allowed region - for i_min in range(box_min[0], box_max[0]+1, 1): - if all(i_min*ineqs[:,0]+tmp_v >= 0): + for i_min in range(box_min[0], box_max[0] + 1, 1): + if all(i_min * ineqs[:, 0] + tmp_v >= 0): break # Find the upper bound for the allowed region - for i_max in range(box_max[0], i_min-1, -1): - if all(i_max*ineqs[:,0]+tmp_v >= 0): + for i_max in range(box_max[0], i_min - 1, -1): + if all(i_max * ineqs[:, 0] + tmp_v >= 0): break else: i_max -= 1 # The points i_min .. i_max are contained in the polytope - for i in range(i_min, i_max+1): + for i in range(i_min, i_max + 1): p[0] = i pts_all.append(np.array(p)[orig_perm]) - saturated = frozenset(j for j in range(len(tmp_v)) - if i*ineqs[j,0] + tmp_v[j] == 0) + saturated = frozenset( + j for j in range(len(tmp_v)) if i * ineqs[j, 0] + tmp_v[j] == 0 + ) facet_ind.append(saturated) # Increment the other entries in p to move on to next loop inc = 1 if dim == 1: break - + while p[inc] == box_max[inc]: p[inc] = box_min[inc] inc += 1 diff --git a/src/cytools/polytopeface.py b/src/cytools/polytopeface.py index a2b4973..715ba6f 100644 --- a/src/cytools/polytopeface.py +++ b/src/cytools/polytopeface.py @@ -30,6 +30,7 @@ from cytools.triangulation import Triangulation from cytools.utils import lll_reduce + class PolytopeFace: """ This class handles all computations relating to faces of lattice polytopes. @@ -69,11 +70,13 @@ class PolytopeFace: ``` """ - def __init__(self, - ambient_poly: "Polytope", - vert_labels: list, - saturated_ineqs: frozenset, - dim: int = None) -> None: + def __init__( + self, + ambient_poly: "Polytope", + vert_labels: list, + saturated_ineqs: frozenset, + dim: int = None, + ) -> None: """ **Description:** Initializes a `PolytopeFace` object. @@ -113,7 +116,7 @@ def __init__(self, self._dim = dim else: verts = ambient_poly.points(which=vert_labels) - self._dim = np.linalg.matrix_rank([list(pt)+[1] for pt in verts])-1 + self._dim = np.linalg.matrix_rank([list(pt) + [1] for pt in verts]) - 1 # defaults # ======== @@ -138,9 +141,11 @@ def __repr__(self) -> str: print(f) # Prints face info ``` """ - return (f"A {self.dim()}-dimensional face of a " - f"{self.ambient_poly.dim()}-dimensional polytope in " - f"ZZ^{self.ambient_dim()}") + return ( + f"A {self.dim()}-dimensional face of a " + f"{self.ambient_poly.dim()}-dimensional polytope in " + f"ZZ^{self.ambient_dim()}" + ) # caching # ======= @@ -190,7 +195,8 @@ def ambient_poly(self) -> "Polytope": The ambient polytope. """ return self._ambient_poly - ambient_polytope = lambda self:self.ambient_poly + + ambient_polytope = lambda self: self.ambient_poly @property def labels(self) -> tuple: @@ -272,6 +278,7 @@ def dimension(self) -> int: `dim`. """ return self._dim + # aliases dim = dimension @@ -290,6 +297,7 @@ def ambient_dimension(self) -> int: `ambient_dim`. """ return self.ambient_poly.ambient_dimension() + # aliases ambient_dim = ambient_dimension @@ -334,10 +342,9 @@ def _process_points(self) -> None: self._labels_int = tuple(self._labels_int) self._labels_bdry = tuple(self._labels_bdry) - def points(self, - which = None, - optimal: bool = False, - as_indices: bool = False) -> np.ndarray: + def points( + self, which=None, optimal: bool = False, as_indices: bool = False + ) -> np.ndarray: """ **Description:** Returns the lattice points of the face. @@ -371,22 +378,25 @@ def points(self, else: # check if the input labels if not set(which).issubset(self.labels): - raise ValueError(f"Specified labels ({which}) aren't subset "\ - f"of the face lables ({self.labels})...") + raise ValueError( + f"Specified labels ({which}) aren't subset " + f"of the face lables ({self.labels})..." + ) # return if optimal and (not as_indices): - dim_diff = self.ambient_dim()-self.dim() - if dim_diff>0: + dim_diff = self.ambient_dim() - self.dim() + if dim_diff > 0: # asking for optimal points, where the optimal value may # differ from the entire polytope pts = self.points(which=which) - return lll_reduce(pts-pts[0])[:,dim_diff:] - + return lll_reduce(pts - pts[0])[:, dim_diff:] + # normal case - return self.ambient_poly.points(which=which, - optimal=optimal, - as_indices=as_indices) + return self.ambient_poly.points( + which=which, optimal=optimal, as_indices=as_indices + ) + # aliases pts = points @@ -415,8 +425,8 @@ def interior_points(self, as_indices: bool = False) -> ArrayLike: # [ 0, 0, 0, -1]]) ``` """ - return self.ambient_poly.points(which=self.labels_int, - as_indices=as_indices) + return self.ambient_poly.points(which=self.labels_int, as_indices=as_indices) + # aliases interior_pts = interior_points @@ -447,8 +457,8 @@ def boundary_points(self, as_indices: bool = False) -> ArrayLike: # [ 0, 1, 0, 0]]) ``` """ - return self.ambient_poly.points(which=self.labels_bdry, - as_indices=as_indices) + return self.ambient_poly.points(which=self.labels_bdry, as_indices=as_indices) + # aliases boundary_pts = boundary_points @@ -474,8 +484,9 @@ def vertices(self, as_indices: bool = False) -> ArrayLike: # [ 0, 0, 1, 0]]) ``` """ - return self.ambient_poly.points(which=self._labels_vertices, - as_indices=as_indices) + return self.ambient_poly.points( + which=self._labels_vertices, as_indices=as_indices + ) # polytope # ======== @@ -503,10 +514,14 @@ def as_polytope(self) -> "Polytope": """ if self._polytope is None: from cytools.polytope import Polytope - self._polytope = Polytope(self.points(), - labels=self.labels, - backend=self.ambient_poly.backend) + + self._polytope = Polytope( + self.points(), + labels=self.labels, + backend=self.ambient_poly.backend, + ) return self._polytope + # alias as_poly = as_polytope @@ -553,20 +568,23 @@ def dual_face(self) -> "PolytopeFace": # perform the calculation dual_poly = self.ambient_poly.dual() - dual_vert = self.ambient_poly.inequalities()[ - list(self._saturated_ineqs),:-1] - dual_ineqs = dual_poly.inequalities()[:,:-1].tolist() - dual_saturated_ineqs = frozenset([dual_ineqs.index(v) - for v in self.vertices().tolist()]) + dual_vert = self.ambient_poly.inequalities()[list(self._saturated_ineqs), :-1] + dual_ineqs = dual_poly.inequalities()[:, :-1].tolist() + dual_saturated_ineqs = frozenset( + [dual_ineqs.index(v) for v in self.vertices().tolist()] + ) dual_face_dim = self.ambient_poly._dim - self._dim - 1 - self._dual_face = PolytopeFace(dual_poly, - dual_poly.points_to_labels(dual_vert), - dual_saturated_ineqs, - dim=dual_face_dim) + self._dual_face = PolytopeFace( + dual_poly, + dual_poly.points_to_labels(dual_vert), + dual_saturated_ineqs, + dim=dual_face_dim, + ) self._dual_face._dual_face = self # return return self.dual_face() + # aliases dual = dual_face @@ -602,13 +620,18 @@ def faces(self, d: int = None) -> tuple: # return answer if known if self._faces is not None: - return (self._faces[d] if d is not None else self._faces) + return self._faces[d] if d is not None else self._faces # calculate the answer faces = [] for dd in range(self._dim + 1): - faces.append(tuple(f for f in self.ambient_poly.faces(dd) - if self._saturated_ineqs.issubset(f._saturated_ineqs))) + faces.append( + tuple( + f + for f in self.ambient_poly.faces(dd) + if self._saturated_ineqs.issubset(f._saturated_ineqs) + ) + ) self._faces = tuple(faces) # return @@ -616,12 +639,14 @@ def faces(self, d: int = None) -> tuple: # triangulating # ============= - def triangulate(self, - heights: list = None, - simplices: ArrayLike = None, - check_input_simplices: bool = True, - backend: str = "cgal", - verbosity=0) -> "Triangulation": + def triangulate( + self, + heights: list = None, + simplices: ArrayLike = None, + check_input_simplices: bool = True, + backend: str = "cgal", + verbosity=0, + ) -> "Triangulation": """ **Description:** Returns a single regular triangulation of the face. @@ -652,11 +677,13 @@ def triangulate(self, A [`Triangulation`](./triangulation) object describing a triangulation of the polytope. """ - return Triangulation(self.ambient_poly, - self.labels, - make_star=False, - heights=heights, - simplices=simplices, - check_input_simplices=check_input_simplices, - backend=backend, - verbosity=verbosity) + return Triangulation( + self.ambient_poly, + self.labels, + make_star=False, + heights=heights, + simplices=simplices, + check_input_simplices=check_input_simplices, + backend=backend, + verbosity=verbosity, + ) diff --git a/src/cytools/toricvariety.py b/src/cytools/toricvariety.py index 5151419..0409407 100644 --- a/src/cytools/toricvariety.py +++ b/src/cytools/toricvariety.py @@ -33,12 +33,20 @@ from cytools import config from cytools.calabiyau import CalabiYau from cytools.cone import Cone -from cytools.utils import (gcd_list, solve_linear_system, - array_float_to_fmpq, - array_fmpq_to_float, filter_tensor_indices, - symmetric_sparse_to_dense, float_to_fmpq, - symmetric_dense_to_sparse, fmpq_to_float, - set_divisor_basis, set_curve_basis) +from cytools.utils import ( + gcd_list, + solve_linear_system, + array_float_to_fmpq, + array_fmpq_to_float, + filter_tensor_indices, + symmetric_sparse_to_dense, + float_to_fmpq, + symmetric_dense_to_sparse, + fmpq_to_float, + set_divisor_basis, + set_curve_basis, +) + class ToricVariety: """ @@ -118,9 +126,11 @@ def __init__(self, triang): if not triang.is_star(): raise ValueError("The input triangulation must be star.") if not triang.polytope().is_reflexive() and not config._exp_features_enabled: - raise Exception("The experimental features must be enabled to " - "construct toric varieties from triangulations " - "that are not from reflexive polytopes.") + raise Exception( + "The experimental features must be enabled to " + "construct toric varieties from triangulations " + "that are not from reflexive polytopes." + ) self._triang = triang # Initialize remaining hidden attributes self._hash = None @@ -130,7 +140,7 @@ def __init__(self, triang): self._divisor_basis_mat = None self._curve_basis = None self._curve_basis_mat = None - self._mori_cone = [None]*3 + self._mori_cone = [None] * 3 self._intersection_numbers = dict() self._prime_divs = None self._is_compact = None @@ -141,8 +151,10 @@ def __init__(self, triang): self._nef_part = None self._cy = None if not self.is_compact() and not config._exp_features_enabled: - raise Exception("The experimental features must be enabled to " - "construct non-compact varieties.") + raise Exception( + "The experimental features must be enabled to " + "construct non-compact varieties." + ) def clear_cache(self, recursive=False, only_in_basis=False): """ @@ -186,7 +198,7 @@ def clear_cache(self, recursive=False, only_in_basis=False): self._divisor_basis_mat = None self._curve_basis = None self._curve_basis_mat = None - self._mori_cone = [None]*3 + self._mori_cone = [None] * 3 self._intersection_numbers = dict() self._prime_divs = None self._is_compact = None @@ -221,10 +233,12 @@ def __repr__(self): # A smooth compact 4-dimensional toric variety with 5 affine patches ``` """ - out_str = (f"A {'smooth' if self.is_smooth() else 'simplicial'} " - f"{'' if self.is_compact() else 'non-'}compact {self.dim()}" - f"-dimensional toric variety with {len(self.triangulation().simplices())}" - f" affine patches") + out_str = ( + f"A {'smooth' if self.is_smooth() else 'simplicial'} " + f"{'' if self.is_compact() else 'non-'}compact {self.dim()}" + f"-dimensional toric variety with {len(self.triangulation().simplices())}" + f" affine patches" + ) return out_str def __eq__(self, other): @@ -309,7 +323,7 @@ def __hash__(self): """ if self._hash is not None: return self._hash - self._hash = hash((1,hash(self.triangulation()))) + self._hash = hash((1, hash(self.triangulation()))) return self._hash def is_compact(self): @@ -335,7 +349,9 @@ def is_compact(self): """ if self._is_compact is not None: return self._is_compact - self._is_compact = (0,)*self.dim() in [tuple(pt) for pt in self.polytope().interior_points()] + self._is_compact = (0,) * self.dim() in [ + tuple(pt) for pt in self.polytope().interior_points() + ] return self._is_compact def triangulation(self): @@ -413,6 +429,7 @@ def dimension(self): ``` """ return self.triangulation().dim() + # aliases dim = dimension @@ -466,11 +483,12 @@ def glsm_charge_matrix(self, include_origin=True): ``` """ if self._glsm_charge_matrix is not None: - return np.array(self._glsm_charge_matrix)[:,(0 if include_origin else 1):] + return np.array(self._glsm_charge_matrix)[:, (0 if include_origin else 1) :] self._glsm_charge_matrix = self.polytope().glsm_charge_matrix( - include_origin=True, - points=self.polytope().points_to_indices(self.triangulation().points())) - return np.array(self._glsm_charge_matrix)[:,(0 if include_origin else 1):] + include_origin=True, + points=self.polytope().points_to_indices(self.triangulation().points()), + ) + return np.array(self._glsm_charge_matrix)[:, (0 if include_origin else 1) :] def glsm_linear_relations(self, include_origin=True): """ @@ -508,11 +526,16 @@ def glsm_linear_relations(self, include_origin=True): ``` """ if self._glsm_linrels is not None: - return np.array(self._glsm_linrels)[(0 if include_origin else 1):,(0 if include_origin else 1):] + return np.array(self._glsm_linrels)[ + (0 if include_origin else 1) :, (0 if include_origin else 1) : + ] self._glsm_linrels = self.polytope().glsm_linear_relations( - include_origin=True, - points=self.polytope().points_to_indices(self.triangulation().points())) - return np.array(self._glsm_linrels)[(0 if include_origin else 1):,(0 if include_origin else 1):] + include_origin=True, + points=self.polytope().points_to_indices(self.triangulation().points()), + ) + return np.array(self._glsm_linrels)[ + (0 if include_origin else 1) :, (0 if include_origin else 1) : + ] def divisor_basis(self, include_origin=True, as_matrix=False): """ @@ -554,17 +577,25 @@ def divisor_basis(self, include_origin=True, as_matrix=False): ``` """ if self._divisor_basis is None: - self.set_divisor_basis(self.polytope().glsm_basis( - integral=True, - include_origin=True, - points=self.polytope().points_to_indices(self.triangulation().points())) - ) + self.set_divisor_basis( + self.polytope().glsm_basis( + integral=True, + include_origin=True, + points=self.polytope().points_to_indices( + self.triangulation().points() + ), + ) + ) if len(self._divisor_basis.shape) == 1: if 0 in self._divisor_basis and not include_origin: - raise Exception("The basis was requested not including the " - "origin, but it is included in the current basis.") + raise Exception( + "The basis was requested not including the " + "origin, but it is included in the current basis." + ) if as_matrix: - return np.array(self._divisor_basis_mat[:,(0 if include_origin else 1):]) + return np.array( + self._divisor_basis_mat[:, (0 if include_origin else 1) :] + ) return np.array(self._divisor_basis) - (0 if include_origin else 1) return np.array(self._divisor_basis) @@ -657,17 +688,25 @@ def curve_basis(self, include_origin=True, as_matrix=False): ``` """ if self._curve_basis is None: - self.set_divisor_basis(self.polytope().glsm_basis( - integral=True, - include_origin=True, - points=self.polytope().points_to_indices(self.triangulation().points())) - ) + self.set_divisor_basis( + self.polytope().glsm_basis( + integral=True, + include_origin=True, + points=self.polytope().points_to_indices( + self.triangulation().points() + ), + ) + ) if len(self._curve_basis.shape) == 1: if 0 in self._curve_basis and not include_origin: - raise Exception("The basis was requested not including the " - "origin, but it is included in the current basis.") + raise Exception( + "The basis was requested not including the " + "origin, but it is included in the current basis." + ) if as_matrix: - return np.array(self._curve_basis_mat[:,(0 if include_origin else 1):]) + return np.array( + self._curve_basis_mat[:, (0 if include_origin else 1) :] + ) return np.array(self._curve_basis) - (0 if include_origin else 1) return np.array(self._curve_basis) @@ -726,8 +765,12 @@ def set_curve_basis(self, basis, include_origin=True): # shared with the CalabiYau class. set_curve_basis(self, basis, include_origin=include_origin) - def mori_cone(self, in_basis=False, include_origin=True, - from_intersection_numbers=False): + def mori_cone( + self, + in_basis=False, + include_origin=True, + from_intersection_numbers=False, + ): """ **Description:** Returns the Mori cone of the toric variety. @@ -763,14 +806,16 @@ def mori_cone(self, in_basis=False, include_origin=True, """ if self._mori_cone[0] is None: if from_intersection_numbers: - rays = (self._compute_mori_rays_from_intersections_4d() - if self.dim() == 4 else - self._compute_mori_rays_from_intersections()) + rays = ( + self._compute_mori_rays_from_intersections_4d() + if self.dim() == 4 + else self._compute_mori_rays_from_intersections() + ) self._mori_cone[0] = Cone(rays) else: self._mori_cone[0] = self.triangulation().secondary_cone().dual() # 0: All divs, 1: No origin, 2: In basis - args_id = ((not include_origin)*1 if not in_basis else 0) + in_basis*2 + args_id = ((not include_origin) * 1 if not in_basis else 0) + in_basis * 2 if self._mori_cone[args_id] is not None: return self._mori_cone[args_id] rays = self._mori_cone[0].rays() @@ -778,13 +823,13 @@ def mori_cone(self, in_basis=False, include_origin=True, if include_origin and not in_basis: new_rays = rays elif not include_origin and not in_basis: - new_rays = rays[:,1:] + new_rays = rays[:, 1:] else: - if len(basis.shape) == 2: # If basis is matrix + if len(basis.shape) == 2: # If basis is matrix new_rays = rays.dot(basis.T) else: - new_rays = rays[:,basis] - c = Cone(new_rays, check=len(basis.shape)==2) + new_rays = rays[:, basis] + c = Cone(new_rays, check=len(basis.shape) == 2) self._mori_cone[args_id] = c return self._mori_cone[args_id] @@ -821,27 +866,29 @@ def _compute_mori_rays_from_intersections(self): intnums = self.intersection_numbers(in_basis=False) dim = self.dim() num_divs = self.glsm_charge_matrix().shape[1] - curve_dict = defaultdict(lambda: [[],[]]) + curve_dict = defaultdict(lambda: [[], []]) for ii in intnums: if 0 in ii: continue ctr = Counter(ii) - if len(ctr) < dim-1: + if len(ctr) < dim - 1: continue - for comb in set(combinations(ctr.keys(),dim-1)): + for comb in set(combinations(ctr.keys(), dim - 1)): crv = tuple(sorted(comb)) - curve_dict[crv][0].append(int(sum([i*(ctr[i]-(i in crv)) for i in ctr]))) + curve_dict[crv][0].append( + int(sum([i * (ctr[i] - (i in crv)) for i in ctr])) + ) curve_dict[crv][1].append(intnums[ii]) row_set = set() for crv in curve_dict: g = gcd_list(curve_dict[crv][1]) row = np.zeros(num_divs, dtype=int) - for j,jj in enumerate(curve_dict[crv][0]): - row[jj] = int(round(curve_dict[crv][1][j]/g)) + for j, jj in enumerate(curve_dict[crv][0]): + row[jj] = int(round(curve_dict[crv][1][j] / g)) row_set.add(tuple(row)) mori_rays = np.array(list(row_set), dtype=int) # Compute column corresponding to the origin - mori_rays[:,0] = -np.sum(mori_rays, axis=1) + mori_rays[:, 0] = -np.sum(mori_rays, axis=1) return mori_rays def _compute_mori_rays_from_intersections_4d(self): @@ -893,68 +940,84 @@ def _compute_mori_rays_from_intersections_4d(self): if ii[0] == ii[1] and ii[2] == ii[3]: continue if ii[0] == ii[1]: - if (ii[0],ii[2],ii[3]) not in curve_dict.keys(): - curve_dict[(ii[0],ii[2],ii[3])] = curve_ctr - curve_sparse_list.append([curve_ctr,ii[1],intnums[ii]]) + if (ii[0], ii[2], ii[3]) not in curve_dict.keys(): + curve_dict[(ii[0], ii[2], ii[3])] = curve_ctr + curve_sparse_list.append([curve_ctr, ii[1], intnums[ii]]) curve_ctr += 1 else: - curve_sparse_list.append([curve_dict[(ii[1],ii[2],ii[3])],ii[0],intnums[ii]]) + curve_sparse_list.append( + [curve_dict[(ii[1], ii[2], ii[3])], ii[0], intnums[ii]] + ) elif ii[1] == ii[2]: - if (ii[0],ii[1],ii[3]) not in curve_dict.keys(): - curve_dict[(ii[0],ii[1],ii[3])] = curve_ctr - curve_sparse_list.append([curve_ctr,ii[2],intnums[ii]]) + if (ii[0], ii[1], ii[3]) not in curve_dict.keys(): + curve_dict[(ii[0], ii[1], ii[3])] = curve_ctr + curve_sparse_list.append([curve_ctr, ii[2], intnums[ii]]) curve_ctr += 1 else: - curve_sparse_list.append([curve_dict[(ii[0],ii[1],ii[3])],ii[2],intnums[ii]]) + curve_sparse_list.append( + [curve_dict[(ii[0], ii[1], ii[3])], ii[2], intnums[ii]] + ) elif ii[2] == ii[3]: - if (ii[0],ii[1],ii[2]) not in curve_dict.keys(): - curve_dict[(ii[0],ii[1],ii[2])] = curve_ctr - curve_sparse_list.append([curve_ctr,ii[3],intnums[ii]]) + if (ii[0], ii[1], ii[2]) not in curve_dict.keys(): + curve_dict[(ii[0], ii[1], ii[2])] = curve_ctr + curve_sparse_list.append([curve_ctr, ii[3], intnums[ii]]) curve_ctr += 1 else: - curve_sparse_list.append([curve_dict[(ii[0],ii[1],ii[2])],ii[3],intnums[ii]]) + curve_sparse_list.append( + [curve_dict[(ii[0], ii[1], ii[2])], ii[3], intnums[ii]] + ) else: - if (ii[0],ii[1],ii[2]) not in curve_dict.keys(): - curve_dict[(ii[0],ii[1],ii[2])] = curve_ctr - curve_sparse_list.append([curve_ctr,ii[3],intnums[ii]]) + if (ii[0], ii[1], ii[2]) not in curve_dict.keys(): + curve_dict[(ii[0], ii[1], ii[2])] = curve_ctr + curve_sparse_list.append([curve_ctr, ii[3], intnums[ii]]) curve_ctr += 1 else: - curve_sparse_list.append([curve_dict[(ii[0],ii[1],ii[2])],ii[3],intnums[ii]]) - if (ii[0],ii[1],ii[3]) not in curve_dict.keys(): - curve_dict[(ii[0],ii[1],ii[3])] = curve_ctr - curve_sparse_list.append([curve_ctr,ii[2],intnums[ii]]) + curve_sparse_list.append( + [curve_dict[(ii[0], ii[1], ii[2])], ii[3], intnums[ii]] + ) + if (ii[0], ii[1], ii[3]) not in curve_dict.keys(): + curve_dict[(ii[0], ii[1], ii[3])] = curve_ctr + curve_sparse_list.append([curve_ctr, ii[2], intnums[ii]]) curve_ctr += 1 else: - curve_sparse_list.append([curve_dict[(ii[0],ii[1],ii[3])],ii[2],intnums[ii]]) - if (ii[0],ii[2],ii[3]) not in curve_dict.keys(): - curve_dict[(ii[0],ii[2],ii[3])] = curve_ctr - curve_sparse_list.append([curve_ctr,ii[1],intnums[ii]]) + curve_sparse_list.append( + [curve_dict[(ii[0], ii[1], ii[3])], ii[2], intnums[ii]] + ) + if (ii[0], ii[2], ii[3]) not in curve_dict.keys(): + curve_dict[(ii[0], ii[2], ii[3])] = curve_ctr + curve_sparse_list.append([curve_ctr, ii[1], intnums[ii]]) curve_ctr += 1 else: - curve_sparse_list.append([curve_dict[(ii[0],ii[2],ii[3])],ii[1],intnums[ii]]) - if (ii[1],ii[2],ii[3]) not in curve_dict.keys(): - curve_dict[(ii[1],ii[2],ii[3])] = curve_ctr - curve_sparse_list.append([curve_ctr,ii[0],intnums[ii]]) + curve_sparse_list.append( + [curve_dict[(ii[0], ii[2], ii[3])], ii[1], intnums[ii]] + ) + if (ii[1], ii[2], ii[3]) not in curve_dict.keys(): + curve_dict[(ii[1], ii[2], ii[3])] = curve_ctr + curve_sparse_list.append([curve_ctr, ii[0], intnums[ii]]) curve_ctr += 1 else: - curve_sparse_list.append([curve_dict[(ii[1],ii[2],ii[3])],ii[0],intnums[ii]]) + curve_sparse_list.append( + [curve_dict[(ii[1], ii[2], ii[3])], ii[0], intnums[ii]] + ) row_list = [[] for i in range(curve_ctr)] # Remove zeros for ii in curve_sparse_list: - if ii[2]!=0: - row_list[ii[0]].append([ii[1],ii[2]]) + if ii[2] != 0: + row_list[ii[0]].append([ii[1], ii[2]]) # Normalize for row in row_list: g = abs(gcd_list([ii[1] for ii in row])) for ii in row: - ii[1] = int(round(ii[1]/g)) - row_list = set(tuple(tuple(tuple(ii) for ii in sorted(row)) for row in row_list)) - mori_rays = np.zeros((len(row_list),num_divs), dtype=int) - for i,row in enumerate(row_list): + ii[1] = int(round(ii[1] / g)) + row_list = set( + tuple(tuple(tuple(ii) for ii in sorted(row)) for row in row_list) + ) + mori_rays = np.zeros((len(row_list), num_divs), dtype=int) + for i, row in enumerate(row_list): for ii in row: - mori_rays[i,int(round(ii[0]))] = round(ii[1]) + mori_rays[i, int(round(ii[0]))] = round(ii[1]) # Compute column corresponding to the origin - mori_rays[:,0] = -np.sum(mori_rays, axis=1) + mori_rays[:, 0] = -np.sum(mori_rays, axis=1) return mori_rays def kahler_cone(self): @@ -1009,20 +1072,27 @@ def _construct_intnum_equations_4d(self): ``` """ # Origin is at index 0 - pts_ext = np.empty((self.triangulation().points().shape[0], - self.triangulation().points().shape[1]+1), - dtype=int) - pts_ext[:,:-1] = self.triangulation().points() - pts_ext[:,-1] = 1 + pts_ext = np.empty( + ( + self.triangulation().points().shape[0], + self.triangulation().points().shape[1] + 1, + ), + dtype=int, + ) + pts_ext[:, :-1] = self.triangulation().points() + pts_ext[:, -1] = 1 linear_relations = self.glsm_linear_relations(include_origin=False) # First compute the distict intersection numbers - distintnum_array = sorted([ - [c for c in simp if c!=0] - + [1/abs(np.linalg.det([pts_ext[p] for p in simp]))] - for simp in self.triangulation().simplices()]) + distintnum_array = sorted( + [ + [c for c in simp if c != 0] + + [1 / abs(np.linalg.det([pts_ext[p] for p in simp]))] + for simp in self.triangulation().simplices() + ] + ) frst = [[c for c in s if c != 0] for s in self.triangulation().simplices()] - simp_2 = set([j for i in [list(combinations(f,2)) for f in frst] for j in i]) - simp_3 = set([j for i in [list(combinations(f,3)) for f in frst] for j in i]) + simp_2 = set([j for i in [list(combinations(f, 2)) for f in frst] for j in i]) + simp_3 = set([j for i in [list(combinations(f, 3)) for f in frst] for j in i]) # We construct and solve the linear system M*x + C = 0, where M is # a rectangular mxn matrix and C is a vector. ################################################################### @@ -1031,80 +1101,97 @@ def _construct_intnum_equations_4d(self): ## Dictionary of variables # Most intersection numbers are trivially zero, find the possibly # nonzero intersection numbers. - variable_array_1 = [tuple(j) for i in [[[s[0],s[0],s[1],s[2]], - [s[0],s[1],s[1],s[2]], - [s[0],s[1],s[2],s[2]]] - for s in simp_3] for j in i] - variable_array_2 = [tuple(j) for i in [[[s[0],s[0],s[1],s[1]], - [s[0],s[0],s[0],s[1]], - [s[0],s[1],s[1],s[1]]] - for s in simp_2] for j in i] - variable_array_3 = [(i,i,i,i) for i in range(1, len(pts_ext))] - variable_array = sorted(variable_array_1 + variable_array_2 - + variable_array_3) - variable_dict = {vv:v for v,vv in enumerate(variable_array)} + variable_array_1 = [ + tuple(j) + for i in [ + [ + [s[0], s[0], s[1], s[2]], + [s[0], s[1], s[1], s[2]], + [s[0], s[1], s[2], s[2]], + ] + for s in simp_3 + ] + for j in i + ] + variable_array_2 = [ + tuple(j) + for i in [ + [ + [s[0], s[0], s[1], s[1]], + [s[0], s[0], s[0], s[1]], + [s[0], s[1], s[1], s[1]], + ] + for s in simp_2 + ] + for j in i + ] + variable_array_3 = [(i, i, i, i) for i in range(1, len(pts_ext))] + variable_array = sorted(variable_array_1 + variable_array_2 + variable_array_3) + variable_dict = {vv: v for v, vv in enumerate(variable_array)} ## Dictionary to construct C # C is constructed by adding/subtracting distinct intersection # numbers. - c_dict = {s:[] for s in simp_3} + c_dict = {s: [] for s in simp_3} for d in distintnum_array: - c_dict[(d[0],d[1],d[2])] += [[d[3],d[4]]] - c_dict[(d[0],d[1],d[3])] += [[d[2],d[4]]] - c_dict[(d[0],d[2],d[3])] += [[d[1],d[4]]] - c_dict[(d[1],d[2],d[3])] += [[d[0],d[4]]] + c_dict[(d[0], d[1], d[2])] += [[d[3], d[4]]] + c_dict[(d[0], d[1], d[3])] += [[d[2], d[4]]] + c_dict[(d[0], d[2], d[3])] += [[d[1], d[4]]] + c_dict[(d[1], d[2], d[3])] += [[d[0], d[4]]] ## Dictionary to construct M eqn_array_1 = [tuple(s) for s in simp_3] - eqn_array_2 = [tuple(j) for i in [[[s[0],s[0],s[1]], - [s[0],s[1],s[1]]] - for s in simp_2] for j in i] - eqn_array_3 = [(i,i,i) for i in range(1, len(pts_ext))] + eqn_array_2 = [ + tuple(j) + for i in [[[s[0], s[0], s[1]], [s[0], s[1], s[1]]] for s in simp_2] + for j in i + ] + eqn_array_3 = [(i, i, i) for i in range(1, len(pts_ext))] eqn_array = sorted(eqn_array_1 + eqn_array_2 + eqn_array_3) - eqn_dict = {eq:[] for eq in eqn_array} + eqn_dict = {eq: [] for eq in eqn_array} for v in variable_array: - if v[0]==v[3]: - eqn_dict[(v[0],v[1],v[2])] += [[v[3],variable_dict[v]]] - elif v[0]==v[2]: - eqn_dict[(v[0],v[1],v[2])] += [[v[3],variable_dict[v]]] - eqn_dict[(v[0],v[1],v[3])] += [[v[2],variable_dict[v]]] - elif v[0]==v[1] and v[2]==v[3]: - eqn_dict[(v[0],v[1],v[2])] += [[v[3],variable_dict[v]]] - eqn_dict[(v[0],v[2],v[3])] += [[v[1],variable_dict[v]]] - elif v[0]==v[1]: - eqn_dict[(v[0],v[1],v[2])] += [[v[3],variable_dict[v]]] - eqn_dict[(v[0],v[1],v[3])] += [[v[2],variable_dict[v]]] - eqn_dict[(v[0],v[2],v[3])] += [[v[1],variable_dict[v]]] - elif v[1]==v[3]: - eqn_dict[(v[0],v[1],v[2])] += [[v[3],variable_dict[v]]] - eqn_dict[(v[1],v[2],v[3])] += [[v[0],variable_dict[v]]] - elif v[1]==v[2]: - eqn_dict[(v[0],v[1],v[2])] += [[v[3],variable_dict[v]]] - eqn_dict[(v[0],v[1],v[3])] += [[v[2],variable_dict[v]]] - eqn_dict[(v[1],v[2],v[3])] += [[v[0],variable_dict[v]]] - elif v[2]==v[3]: - eqn_dict[(v[0],v[1],v[2])] += [[v[3],variable_dict[v]]] - eqn_dict[(v[0],v[2],v[3])] += [[v[1],variable_dict[v]]] - eqn_dict[(v[1],v[2],v[3])] += [[v[0],variable_dict[v]]] + if v[0] == v[3]: + eqn_dict[(v[0], v[1], v[2])] += [[v[3], variable_dict[v]]] + elif v[0] == v[2]: + eqn_dict[(v[0], v[1], v[2])] += [[v[3], variable_dict[v]]] + eqn_dict[(v[0], v[1], v[3])] += [[v[2], variable_dict[v]]] + elif v[0] == v[1] and v[2] == v[3]: + eqn_dict[(v[0], v[1], v[2])] += [[v[3], variable_dict[v]]] + eqn_dict[(v[0], v[2], v[3])] += [[v[1], variable_dict[v]]] + elif v[0] == v[1]: + eqn_dict[(v[0], v[1], v[2])] += [[v[3], variable_dict[v]]] + eqn_dict[(v[0], v[1], v[3])] += [[v[2], variable_dict[v]]] + eqn_dict[(v[0], v[2], v[3])] += [[v[1], variable_dict[v]]] + elif v[1] == v[3]: + eqn_dict[(v[0], v[1], v[2])] += [[v[3], variable_dict[v]]] + eqn_dict[(v[1], v[2], v[3])] += [[v[0], variable_dict[v]]] + elif v[1] == v[2]: + eqn_dict[(v[0], v[1], v[2])] += [[v[3], variable_dict[v]]] + eqn_dict[(v[0], v[1], v[3])] += [[v[2], variable_dict[v]]] + eqn_dict[(v[1], v[2], v[3])] += [[v[0], variable_dict[v]]] + elif v[2] == v[3]: + eqn_dict[(v[0], v[1], v[2])] += [[v[3], variable_dict[v]]] + eqn_dict[(v[0], v[2], v[3])] += [[v[1], variable_dict[v]]] + eqn_dict[(v[1], v[2], v[3])] += [[v[0], variable_dict[v]]] else: raise RuntimeError("Failed to construct linear system.") # Construct Linear System - num_rows = len(linear_relations)*len(eqn_array) - C = np.array([0.0]*num_rows) + num_rows = len(linear_relations) * len(eqn_array) + C = np.array([0.0] * num_rows) M_row = [] M_col = [] M_val = [] row_ctr = 0 for eqn in eqn_array: for lin in linear_relations: - if eqn[0]!=eqn[1] and eqn[1]!=eqn[2]: + if eqn[0] != eqn[1] and eqn[1] != eqn[2]: c_temp = c_dict[eqn] - C[row_ctr] = sum([lin[cc[0]-1]*cc[1] for cc in c_temp]) + C[row_ctr] = sum([lin[cc[0] - 1] * cc[1] for cc in c_temp]) eqn_temp = eqn_dict[eqn] for e in eqn_temp: M_row.append(row_ctr) M_col.append(e[1]) - M_val.append(lin[e[0]-1]) - row_ctr+=1 - Mat = csr_matrix((M_val,(M_row,M_col)), dtype=np.float64) + M_val.append(lin[e[0] - 1]) + row_ctr += 1 + Mat = csr_matrix((M_val, (M_row, M_col)), dtype=np.float64) return Mat, C, distintnum_array, variable_array def _construct_intnum_equations(self): @@ -1135,18 +1222,23 @@ def _construct_intnum_equations(self): ``` """ dim = self.dim() - pts_ext = np.empty((self.triangulation().points().shape[0],dim+1), dtype=int) - pts_ext[:,:-1] = self.triangulation().points() - pts_ext[:,-1] = 1 + pts_ext = np.empty((self.triangulation().points().shape[0], dim + 1), dtype=int) + pts_ext[:, :-1] = self.triangulation().points() + pts_ext[:, -1] = 1 linear_relations = self.glsm_linear_relations(include_origin=False) # First compute the distict intersection numbers - distintnum_array = sorted([ - [c for c in simp if c!=0] - + [1/abs(np.linalg.det([pts_ext[p] for p in simp]))] - for simp in self.triangulation().simplices()]) + distintnum_array = sorted( + [ + [c for c in simp if c != 0] + + [1 / abs(np.linalg.det([pts_ext[p] for p in simp]))] + for simp in self.triangulation().simplices() + ] + ) frst = [[c for c in s if c != 0] for s in self.triangulation().simplices()] - simp_n = [set([j for i in [list(combinations(f,n)) for f in frst] - for j in i]) for n in range(2,dim)] + simp_n = [ + set([j for i in [list(combinations(f, n)) for f in frst] for j in i]) + for n in range(2, dim) + ] simp_n = [[np.array(c) for c in simp_n[n]] for n in range(len(simp_n))] # We construct and solve the linear system M*x + C = 0, where M is # a rectangular mxn matrix and C is a vector. @@ -1157,53 +1249,62 @@ def _construct_intnum_equations(self): # Most intersection numbers are trivially zero, find the possibly # nonzero intersection numbers. choices_n = [] - for n in range(2,dim): - comb = list(combinations(range(dim-1),dim-n)) - choices = np.empty((len(comb),dim), dtype=int) - choices[:,0] = 0 - for k,c in enumerate(comb): - for i in range(1,dim): - choices[k,i] = choices[k,i-1] + (0 if i-1 in c else 1) + for n in range(2, dim): + comb = list(combinations(range(dim - 1), dim - n)) + choices = np.empty((len(comb), dim), dtype=int) + choices[:, 0] = 0 + for k, c in enumerate(comb): + for i in range(1, dim): + choices[k, i] = choices[k, i - 1] + (0 if i - 1 in c else 1) choices_n.append(choices) - variable_array_1 = [(i,)*dim for i in range(1,len(pts_ext))] - variable_array_n = [tuple(s[ch]) for n in range(len(simp_n)) - for s in simp_n[n] for ch in choices_n[n]] + variable_array_1 = [(i,) * dim for i in range(1, len(pts_ext))] + variable_array_n = [ + tuple(s[ch]) + for n in range(len(simp_n)) + for s in simp_n[n] + for ch in choices_n[n] + ] variable_array = variable_array_1 + variable_array_n - variable_dict = {vv:v for v,vv in enumerate(variable_array)} + variable_dict = {vv: v for v, vv in enumerate(variable_array)} ## Dictionary to construct C # C is constructed by adding/subtracting distinct intersection # numbers. c_dict = defaultdict(lambda: []) for d in distintnum_array: - for i in range(len(d)-1): - c_dict[tuple(c for j,c in enumerate(d[:-1]) if j!= i) - ] += [(d[i],d[-1])] + for i in range(len(d) - 1): + c_dict[tuple(c for j, c in enumerate(d[:-1]) if j != i)] += [ + (d[i], d[-1]) + ] ## Dictionary to construct M eqn_array_1 = [tuple(s) for s in simp_n[-1]] - eqn_array_2 = [(i,)*(dim-1) for i in range(1, len(pts_ext))] + eqn_array_2 = [(i,) * (dim - 1) for i in range(1, len(pts_ext))] choices_n = [] - for n in range(2,dim-1): - comb = list(combinations(range(dim-2),dim-1-n)) - choices = np.empty((len(comb),dim-1), dtype=int) - choices[:,0] = 0 - for k,c in enumerate(comb): - for i in range(1,dim-1): - choices[k,i] = choices[k,i-1] + (0 if i-1 in c else 1) + for n in range(2, dim - 1): + comb = list(combinations(range(dim - 2), dim - 1 - n)) + choices = np.empty((len(comb), dim - 1), dtype=int) + choices[:, 0] = 0 + for k, c in enumerate(comb): + for i in range(1, dim - 1): + choices[k, i] = choices[k, i - 1] + (0 if i - 1 in c else 1) choices_n.append(choices) - eqn_array_n = [tuple(s[ch]) for n in range(len(choices_n)) - for s in simp_n[n] for ch in choices_n[n]] + eqn_array_n = [ + tuple(s[ch]) + for n in range(len(choices_n)) + for s in simp_n[n] + for ch in choices_n[n] + ] eqn_array = eqn_array_1 + eqn_array_2 + eqn_array_n eqn_dict = defaultdict(lambda: []) for v in variable_array: - for c in set(combinations(v,dim-1)): + for c in set(combinations(v, dim - 1)): k = None for i in range(dim): - if i == dim-1 or v[i] != c[i]: + if i == dim - 1 or v[i] != c[i]: k = i break - eqn_dict[c] += [(v[k],variable_dict[v])] + eqn_dict[c] += [(v[k], variable_dict[v])] # Construct Linear System - num_rows = len(linear_relations)*len(eqn_array) + num_rows = len(linear_relations) * len(eqn_array) C = np.zeros(num_rows, dtype=float) M_row = [] M_col = [] @@ -1211,24 +1312,31 @@ def _construct_intnum_equations(self): row_ctr = 0 for eqn in eqn_array: for lin in linear_relations: - if len(set(eqn)) == dim-1: + if len(set(eqn)) == dim - 1: c_temp = c_dict[eqn] - C[row_ctr] = sum([lin[cc[0]-1]*cc[1] for cc in c_temp]) + C[row_ctr] = sum([lin[cc[0] - 1] * cc[1] for cc in c_temp]) eqn_temp = eqn_dict[eqn] for e in eqn_temp: M_row.append(row_ctr) M_col.append(e[1]) - M_val.append(lin[e[0]-1]) - row_ctr+=1 - Mat = csr_matrix((M_val,(M_row,M_col)), dtype=np.float64) + M_val.append(lin[e[0] - 1]) + row_ctr += 1 + Mat = csr_matrix((M_val, (M_row, M_col)), dtype=np.float64) return Mat, C, distintnum_array, variable_array - def intersection_numbers(self, in_basis=False, format="dok", - zero_as_anticanonical=False, backend="all", - check=True, backend_error_tol=1e-3, - round_to_zero_threshold=1e-3, - round_to_integer_error_tol=5e-2, - verbose=0, exact_arithmetic=False): + def intersection_numbers( + self, + in_basis=False, + format="dok", + zero_as_anticanonical=False, + backend="all", + check=True, + backend_error_tol=1e-3, + round_to_zero_threshold=1e-3, + round_to_integer_error_tol=5e-2, + verbose=0, + exact_arithmetic=False, + ): """ **Description:** Returns the intersection numbers of the toric variety. @@ -1336,28 +1444,34 @@ def intersection_numbers(self, in_basis=False, format="dok", ``` """ if format not in ("dok", "coo", "dense"): - raise ValueError("Options for format are \"dok\", \"coo\", \"dense\".") + raise ValueError('Options for format are "dok", "coo", "dense".') if in_basis: zero_as_anticanonical = False args_id = (zero_as_anticanonical, in_basis, exact_arithmetic, format) if args_id in self._intersection_numbers: return copy.copy(self._intersection_numbers[args_id]) - if ((False,False,False,"dok") not in self._intersection_numbers - or ((False,False,True,"dok") not in self._intersection_numbers - and exact_arithmetic)): + if (False, False, False, "dok") not in self._intersection_numbers or ( + (False, False, True, "dok") not in self._intersection_numbers + and exact_arithmetic + ): backends = ["all", "sksparse", "scipy"] if backend not in backends: - raise ValueError("Invalid linear system backend. " - f"The options are: {backends}.") + raise ValueError( + "Invalid linear system backend. " f"The options are: {backends}." + ) if exact_arithmetic and not config._exp_features_enabled: - raise ValueError("The experimental features must be enabled to " - "use exact arithmetic.") + raise ValueError( + "The experimental features must be enabled to " + "use exact arithmetic." + ) # Construct the linear equations # Note that self.dim gives the dimension of the CY not the of the # variety - Mat, C, distintnum_array, variable_array = (self._construct_intnum_equations_4d() - if self.dim() == 4 else - self._construct_intnum_equations()) + Mat, C, distintnum_array, variable_array = ( + self._construct_intnum_equations_4d() + if self.dim() == 4 + else self._construct_intnum_equations() + ) # The system to be solved is Mat*x + C = 0. This is an # overdetermined but consistent linear system. # There is a unique solution to this system. We solve it by @@ -1366,34 +1480,45 @@ def intersection_numbers(self, in_basis=False, format="dok", # MM*x = CC # Since MM is a positive definite full rank matrix, this system can # be solved using via a Cholesky decomposition. - solution = solve_linear_system(Mat, C, backend=backend, check=check, - backend_error_tol=backend_error_tol, - verbosity=verbose) + solution = solve_linear_system( + Mat, + C, + backend=backend, + check=check, + backend_error_tol=backend_error_tol, + verbosity=verbose, + ) if solution is None: raise RuntimeError("Linear system solution failed.") if exact_arithmetic: - solution_fmpq = flint.fmpq_mat([array_float_to_fmpq(solution).tolist()]).transpose() + solution_fmpq = flint.fmpq_mat( + [array_float_to_fmpq(solution).tolist()] + ).transpose() if check: - Mat_fmpq = flint.fmpq_mat(Mat.shape[0],Mat.shape[1]) + Mat_fmpq = flint.fmpq_mat(Mat.shape[0], Mat.shape[1]) Mat_dok = Mat.todok() for k in Mat_dok.keys(): Mat_fmpq[k] = float_to_fmpq(Mat_dok[k]) - C_fmpq = flint.fmpq_mat([array_float_to_fmpq(C).tolist()]).transpose() - res = Mat_fmpq*solution_fmpq + C_fmpq + C_fmpq = flint.fmpq_mat( + [array_float_to_fmpq(C).tolist()] + ).transpose() + res = Mat_fmpq * solution_fmpq + C_fmpq if any(np.array(res.tolist()).flat): raise RuntimeError("Failed to convert to rational numbers.") intnums = dict() if exact_arithmetic: for ii in distintnum_array: - intnums[tuple(int(round(j)) for j in ii[:-1])] = float_to_fmpq(ii[-1]) - for i,ii in enumerate(variable_array): + intnums[tuple(int(round(j)) for j in ii[:-1])] = float_to_fmpq( + ii[-1] + ) + for i, ii in enumerate(variable_array): if abs(solution[i]) < round_to_zero_threshold: continue intnums[tuple(ii)] = float_to_fmpq(solution[i]) else: for ii in distintnum_array: intnums[tuple(int(round(j)) for j in ii[:-1])] = ii[-1] - for i,ii in enumerate(variable_array): + for i, ii in enumerate(variable_array): if abs(solution[i]) < round_to_zero_threshold: continue intnums[tuple(ii)] = solution[i] @@ -1402,14 +1527,18 @@ def intersection_numbers(self, in_basis=False, format="dok", for ii in intnums: c = intnums[ii] if c.q != 1: - raise RuntimeError("Non-integer intersection numbers " - "detected in a smooth toric variety.") + raise RuntimeError( + "Non-integer intersection numbers " + "detected in a smooth toric variety." + ) else: for ii in intnums: c = intnums[ii] - if abs(round(c)-c) > round_to_integer_error_tol: - raise RuntimeError("Non-integer intersection numbers " - "detected in a smooth toric variety.") + if abs(round(c) - c) > round_to_integer_error_tol: + raise RuntimeError( + "Non-integer intersection numbers " + "detected in a smooth toric variety." + ) intnums[ii] = int(round(c)) # Add intersections with canonical divisor # First we only compute intersection numbers with a single index 0 @@ -1419,9 +1548,11 @@ def intersection_numbers(self, in_basis=False, format="dok", dim = self.dim() canon_intnum = defaultdict(lambda: 0) for ii in intnums: - choices = set(tuple(c for i,c in enumerate(ii) if i!=j) for j in range(dim)) + choices = set( + tuple(c for i, c in enumerate(ii) if i != j) for j in range(dim) + ) for c in choices: - canon_intnum[(0,)+c] -= intnums[ii] + canon_intnum[(0,) + c] -= intnums[ii] # Now we round all intersection numbers of the form K_0i...j to # integers if the CY is smooth. Otherwise, we only remove the zero # elements @@ -1429,8 +1560,10 @@ def intersection_numbers(self, in_basis=False, format="dok", for ii in list(canon_intnum.keys()): val = canon_intnum[ii] if val.q != 1: - raise RuntimeError(f"Non-integer intersection numbers " - f"detected in a smooth CY. {ii}:{val}") + raise RuntimeError( + f"Non-integer intersection numbers " + f"detected in a smooth CY. {ii}:{val}" + ) if val != 0: canon_intnum[ii] = val else: @@ -1439,9 +1572,11 @@ def intersection_numbers(self, in_basis=False, format="dok", for ii in list(canon_intnum.keys()): val = canon_intnum[ii] round_val = int(round(val)) - if abs(val-round_val) > round_to_integer_error_tol: - raise RuntimeError(f"Non-integer intersection numbers " - f"detected in a smooth CY. {ii}:{val}") + if abs(val - round_val) > round_to_integer_error_tol: + raise RuntimeError( + f"Non-integer intersection numbers " + f"detected in a smooth CY. {ii}:{val}" + ) if round_val != 0: canon_intnum[ii] = round_val else: @@ -1456,12 +1591,15 @@ def intersection_numbers(self, in_basis=False, format="dok", canon_intnum.pop(ii) # Now we compute remaining intersection numbers canon_intnum_n = [canon_intnum] - for n in range(2,dim+1): + for n in range(2, dim + 1): tmp_intnum = defaultdict(lambda: 0) - for ii,ii_val in canon_intnum_n[-1].items(): - choices = set(tuple(c for i,c in enumerate(ii[n-1:]) if i!=j) for j in range(dim+1-n)) + for ii, ii_val in canon_intnum_n[-1].items(): + choices = set( + tuple(c for i, c in enumerate(ii[n - 1 :]) if i != j) + for j in range(dim + 1 - n) + ) for c in choices: - tmp_intnum[(0,)*n+c] -= ii_val + tmp_intnum[(0,) * n + c] -= ii_val if exact_arithmetic: for ii in list(tmp_intnum.keys()): if tmp_intnum[ii] == 0: @@ -1475,37 +1613,71 @@ def intersection_numbers(self, in_basis=False, format="dok", for ii in canon_intnum_n[i]: intnums[ii] = canon_intnum_n[i][ii] if exact_arithmetic: - self._intersection_numbers[(False,False,True,"dok")] = intnums - self._intersection_numbers[(False,False,False,"dok")] = {ii:(int(intnums[ii].p) if intnums[ii].q==1 - else fmpq_to_float(intnums[ii])) for ii in intnums} + self._intersection_numbers[(False, False, True, "dok")] = intnums + self._intersection_numbers[(False, False, False, "dok")] = { + ii: ( + int(intnums[ii].p) + if intnums[ii].q == 1 + else fmpq_to_float(intnums[ii]) + ) + for ii in intnums + } else: - self._intersection_numbers[(False,False,False,"dok")]= intnums + self._intersection_numbers[(False, False, False, "dok")] = intnums # Now intersection numbers have been computed # We now compute the intersection numbers of the basis if necessary if zero_as_anticanonical and not in_basis: - self._intersection_numbers[args_id] = self._intersection_numbers[(False,False,exact_arithmetic,"dok")] + self._intersection_numbers[args_id] = self._intersection_numbers[ + (False, False, exact_arithmetic, "dok") + ] for ii in self._intersection_numbers[args_id]: if 0 not in ii: continue - self._intersection_numbers[args_id][ii] *= (-1 if sum(ii == 0)%2 == 1 else 1) + self._intersection_numbers[args_id][ii] *= ( + -1 if sum(ii == 0) % 2 == 1 else 1 + ) elif in_basis: basis = self.divisor_basis() - if len(basis.shape) == 2: # If basis is matrix - self._intersection_numbers[(False,True,exact_arithmetic,"dense")] = ( - symmetric_sparse_to_dense(self._intersection_numbers[(False,False,exact_arithmetic,"dok")], basis)) - self._intersection_numbers[(False,True,exact_arithmetic,"dok")] = ( - symmetric_dense_to_sparse(self._intersection_numbers[(False,True,exact_arithmetic,"dense")])) + if len(basis.shape) == 2: # If basis is matrix + self._intersection_numbers[(False, True, exact_arithmetic, "dense")] = ( + symmetric_sparse_to_dense( + self._intersection_numbers[ + (False, False, exact_arithmetic, "dok") + ], + basis, + ) + ) + self._intersection_numbers[(False, True, exact_arithmetic, "dok")] = ( + symmetric_dense_to_sparse( + self._intersection_numbers[ + (False, True, exact_arithmetic, "dense") + ] + ) + ) else: - self._intersection_numbers[(False,True,exact_arithmetic,"dok")] = filter_tensor_indices( - self._intersection_numbers[(False,False,exact_arithmetic,"dok")], basis) + self._intersection_numbers[(False, True, exact_arithmetic, "dok")] = ( + filter_tensor_indices( + self._intersection_numbers[ + (False, False, exact_arithmetic, "dok") + ], + basis, + ) + ) # Intersection numbers of the basis are now done # Finally, we convert into the desired format if format == "coo": - tmpintnums = self._intersection_numbers[(zero_as_anticanonical,in_basis,exact_arithmetic,"dok")] - self._intersection_numbers[args_id] = np.array([list(ii)+[tmpintnums[ii]] for ii in tmpintnums]) + tmpintnums = self._intersection_numbers[ + (zero_as_anticanonical, in_basis, exact_arithmetic, "dok") + ] + self._intersection_numbers[args_id] = np.array( + [list(ii) + [tmpintnums[ii]] for ii in tmpintnums] + ) elif format == "dense": - self._intersection_numbers[args_id] = ( - symmetric_sparse_to_dense(self._intersection_numbers[(zero_as_anticanonical,in_basis,exact_arithmetic,"dok")])) + self._intersection_numbers[args_id] = symmetric_sparse_to_dense( + self._intersection_numbers[ + (zero_as_anticanonical, in_basis, exact_arithmetic, "dok") + ] + ) return copy.copy(self._intersection_numbers[args_id]) def prime_toric_divisors(self): @@ -1532,7 +1704,9 @@ def prime_toric_divisors(self): ``` """ if self._prime_divs is None: - tri_ind = list(set.union(*[set(s) for s in self.triangulation().simplices()])) + tri_ind = list( + set.union(*[set(s) for s in self.triangulation().simplices()]) + ) divs = self.triangulation().triangulation_to_polytope_indices(tri_ind) self._prime_divs = tuple(i for i in divs if i) return self._prime_divs @@ -1567,7 +1741,7 @@ def is_smooth(self): pts = self.triangulation().points() pts = np.insert(pts, 0, np.ones(len(pts), dtype=int), axis=1) simp = self.triangulation().simplices() - self._is_smooth = all(abs(int(round(np.linalg.det(pts[s]))))==1 for s in simp) + self._is_smooth = all(abs(int(round(np.linalg.det(pts[s])))) == 1 for s in simp) return self._is_smooth def canonical_divisor_is_smooth(self): @@ -1597,9 +1771,13 @@ def canonical_divisor_is_smooth(self): pts_mpcp = {tuple(pt) for pt in self.polytope().points_not_interior_to_facets()} ind_triang = list(set.union(*[set(s) for s in self._triang.simplices()])) pts_triang = {tuple(pt) for pt in self._triang.points()[ind_triang]} - sm = (pts_mpcp.issubset(pts_triang) and - (True if self.dim() <= 4 else - all(c.is_smooth() for c in self.fan_cones(self.dim()-1,self.dim()-2)))) + sm = pts_mpcp.issubset(pts_triang) and ( + True + if self.dim() <= 4 + else all( + c.is_smooth() for c in self.fan_cones(self.dim() - 1, self.dim() - 2) + ) + ) self._canon_div_is_smooth = sm return self._canon_div_is_smooth @@ -1627,7 +1805,7 @@ def effective_cone(self): """ if self._eff_cone is not None: return self._eff_cone - self._eff_cone = Cone(self.curve_basis(include_origin=False,as_matrix=True).T) + self._eff_cone = Cone(self.curve_basis(include_origin=False, as_matrix=True).T) return self._eff_cone def fan_cones(self, d=None, face_dim=None): @@ -1661,23 +1839,32 @@ def fan_cones(self, d=None, face_dim=None): ``` """ if d is None: - d = (self.dim() if face_dim is None else face_dim) - if d not in range(1,self.dim()+1): - raise ValueError("Only cones of dimension 1 through d are " - "supported.") - if (d,face_dim) in self._fan_cones: - return self._fan_cones[(d,face_dim)] + d = self.dim() if face_dim is None else face_dim + if d not in range(1, self.dim() + 1): + raise ValueError("Only cones of dimension 1 through d are " "supported.") + if (d, face_dim) in self._fan_cones: + return self._fan_cones[(d, face_dim)] pts = self.triangulation().points() cones = set() - triang_pts_tup = [tuple(pt) for pt in self.triangulation().points()] - faces = ([self.triangulation().points_to_indices([tuple(pt) for pt in f.points() if tuple(pt) in triang_pts_tup]) - for f in self.triangulation()._poly.faces(face_dim)] if face_dim is not None else None) + triang_pts_tup = [tuple(pt) for pt in self.triangulation().points()] + faces = ( + [ + self.triangulation().points_to_indices( + [tuple(pt) for pt in f.points() if tuple(pt) in triang_pts_tup] + ) + for f in self.triangulation()._poly.faces(face_dim) + ] + if face_dim is not None + else None + ) for s in self.triangulation().simplices(): - for c in combinations(s,d): - if (0 not in c and (faces is None or any(all(cc in f for cc in c) for f in faces))): + for c in combinations(s, d): + if 0 not in c and ( + faces is None or any(all(cc in f for cc in c) for f in faces) + ): cones.add(tuple(sorted(c))) - self._fan_cones[(d,face_dim)] = tuple(Cone(pts[list(c)]) for c in cones) - return self._fan_cones[(d,face_dim)] + self._fan_cones[(d, face_dim)] = tuple(Cone(pts[list(c)]) for c in cones) + return self._fan_cones[(d, face_dim)] def get_cy(self, nef_partition=None): """ @@ -1720,8 +1907,9 @@ def get_cy(self, nef_partition=None): return self._cy if nef_partition is not None: if not config._exp_features_enabled: - raise Exception("The experimental features must be enabled to " - "construct CICYs.") + raise Exception( + "The experimental features must be enabled to " "construct CICYs." + ) self._cy = CalabiYau(self, nef_partition) self._nef_part = nef_partition else: @@ -1729,17 +1917,44 @@ def get_cy(self, nef_partition=None): raise ValueError("Triangulation is non-fine.") if not config._exp_features_enabled: if self.dim() != 4: - raise Exception("The experimental features must be enabled to " - "construct CYs with dimension other than 3... " - f"observed dimension = {self.dim()}") + raise Exception( + "The experimental features must be enabled to " + "construct CYs with dimension other than 3... " + f"observed dimension = {self.dim()}" + ) if not self.triangulation().polytope().is_favorable(lattice="N"): - raise Exception("The experimental features must be enabled to " - "construct non-favorable CYs...") - - if not ((self.triangulation().points().shape == self.triangulation().polytope().points_not_interior_to_facets().shape - and all((self.triangulation().points() == self.triangulation().polytope().points_not_interior_to_facets()).flat)) - or (self.triangulation().points().shape == self.triangulation().polytope().points().shape - and all((self.triangulation().points() == self.triangulation().polytope().points()).flat))): + raise Exception( + "The experimental features must be enabled to " + "construct non-favorable CYs..." + ) + + if not ( + ( + self.triangulation().points().shape + == self.triangulation() + .polytope() + .points_not_interior_to_facets() + .shape + and all( + ( + self.triangulation().points() + == self.triangulation() + .polytope() + .points_not_interior_to_facets() + ).flat + ) + ) + or ( + self.triangulation().points().shape + == self.triangulation().polytope().points().shape + and all( + ( + self.triangulation().points() + == self.triangulation().polytope().points() + ).flat + ) + ) + ): error_msg = "Calabi-Yau hypersurfaces must be constructed either from points not interior to facets or using all points.\n" error_msg += f"Triangulation points = {self.triangulation().points().tolist()} (labels = {self.triangulation().labels})\n" error_msg += f"Polytope points = {self.triangulation().polytope().points().tolist()} (labels = {self.triangulation().polytope().labels})\n" diff --git a/src/cytools/triangulation.py b/src/cytools/triangulation.py index db83cea..359de23 100644 --- a/src/cytools/triangulation.py +++ b/src/cytools/triangulation.py @@ -40,6 +40,7 @@ from cytools.toricvariety import ToricVariety from cytools.utils import gcd_list, lll_reduce + class Triangulation: """ This class handles triangulations of lattice polytopes. It can compute @@ -113,14 +114,18 @@ class Triangulation: ``` """ - def __init__(self, - poly: "Polytope", - pts: ArrayLike, - make_star: bool = False, - simplices: ArrayLike=None, check_input_simplices: bool=True, - heights: list = None, check_heights: bool = True, - backend: str = "cgal", - verbosity: int = 1) -> None: + def __init__( + self, + poly: "Polytope", + pts: ArrayLike, + make_star: bool = False, + simplices: ArrayLike = None, + check_input_simplices: bool = True, + heights: list = None, + check_heights: bool = True, + backend: str = "cgal", + verbosity: int = 1, + ) -> None: """ **Description:** Initializes a `Triangulation` object. @@ -174,16 +179,18 @@ def __init__(self, # -------------- # points pts_set = set(pts) - if len(pts_set)==0: + if len(pts_set) == 0: raise ValueError("Need at least 1 point.") elif not pts_set.issubset(poly.labels): raise ValueError("All point labels must exist in the polytope.") # backend backend = backend.lower() - if backend not in ['qhull', 'cgal', 'topcom', None]: - raise ValueError(f"Invalid backend, {backend}. "+\ - f"Options: {['qhull', 'cgal', 'topcom', None]}.") + if backend not in ["qhull", "cgal", "topcom", None]: + raise ValueError( + f"Invalid backend, {backend}. " + + f"Options: {['qhull', 'cgal', 'topcom', None]}." + ) # initialize attributes # --------------------- @@ -204,10 +211,11 @@ def __init__(self, self._labels2optPts = None # dimension - self._dim_ambient = poly.ambient_dim() - self._dim = np.linalg.matrix_rank([list(pt)+[1] for pt in\ - poly.points(which=pts)])-1 - self._is_fulldim = (self._dim == self._dim_ambient) + self._dim_ambient = poly.ambient_dim() + self._dim = ( + np.linalg.matrix_rank([list(pt) + [1] for pt in poly.points(which=pts)]) - 1 + ) + self._is_fulldim = self._dim == self._dim_ambient # simplices if simplices is not None: @@ -220,7 +228,7 @@ def __init__(self, # ------------ # find index of origin if self.poly._label_origin in self.labels: - self._origin_index=list(self.labels).index(self.poly._label_origin) + self._origin_index = list(self.labels).index(self.poly._label_origin) else: # triangulation doesn't include origin self._origin_index = -1 @@ -233,11 +241,13 @@ def __init__(self, self._heights = None # check dimension - if self._simplices.shape[1] != self._dim+1: - simp_dim = self._simplices.shape[1]-1 - error_msg = f"Dimension of simplices, ({simp_dim}), " +\ - "doesn't match polytope dimension (+1), "+\ - f"{self._dim} (+1)..." + if self._simplices.shape[1] != self._dim + 1: + simp_dim = self._simplices.shape[1] - 1 + error_msg = ( + f"Dimension of simplices, ({simp_dim}), " + + "doesn't match polytope dimension (+1), " + + f"{self._dim} (+1)..." + ) raise ValueError(error_msg) if check_input_simplices: @@ -247,15 +257,17 @@ def __init__(self, simp_labels = set(self._simplices.flatten()) if any([(l not in self._labels) for l in simp_labels]): unknown = simp_labels.difference(self._labels) - error_msg = f"Simplices had labels {simp_labels}; " +\ - f"triangulation has labels {self._labels}. " +\ - f"Labels {unknown} are not recognized..." + error_msg = ( + f"Simplices had labels {simp_labels}; " + + f"triangulation has labels {self._labels}. " + + f"Labels {unknown} are not recognized..." + ) raise ValueError(error_msg) - #if min(simp_inds)<0: + # if min(simp_inds)<0: # error_msg = f"A simplex had index, {min(simp_inds)}, " +\ # f"out of range [0,{len(self.points())-1}]" # raise ValueError(error_msg) - #elif max(simp_inds)>=len(self.points()): + # elif max(simp_inds)>=len(self.points()): # error_msg = f"A simplex had index, {max(simp_inds)}, " +\ # f"out of range [0,{len(self.points())-1}]" # raise ValueError(error_msg) @@ -266,31 +278,33 @@ def __init__(self, # ensure simplices define valid triangulation if check_input_simplices: - if not self.is_valid(verbosity=verbosity-1): + if not self.is_valid(verbosity=verbosity - 1): msg = "Simplices don't form valid triangulation." raise ValueError(msg) else: # self._simplices==None... construct simplices from heights - self._is_regular = (None if (backend == "qhull") else True) + self._is_regular = None if (backend == "qhull") else True self._is_valid = True default_triang = heights is None if default_triang: # construct the heights if backend is None: - raise ValueError("Simplices must be specified when working" - "without a backend") + raise ValueError( + "Simplices must be specified when working" "without a backend" + ) # Heights need to be perturbed around the Delaunay heights for # QHull or the triangulation might not be regular. If using # CGAL then they are not perturbed. if backend == "qhull": - heights = [np.dot(p,p) + np.random.normal(0,0.05)\ - for p in self.points()] + heights = [ + np.dot(p, p) + np.random.normal(0, 0.05) for p in self.points() + ] elif backend == "cgal": - heights = [np.dot(p,p) for p in self.points()] - else: # TOPCOM + heights = [np.dot(p, p) for p in self.points()] + else: # TOPCOM heights = None else: # check the heights @@ -303,20 +317,21 @@ def __init__(self, self._heights = np.asarray(heights) # Now run the appropriate triangulation function - triang_pts = self.points(optimal = not self._is_fulldim) + triang_pts = self.points(optimal=not self._is_fulldim) if backend == "qhull": self._simplices = _qhull_triangulate(triang_pts, self._heights) # map to labels - self._simplices = [[self._labels[i] for i in s] for s\ - in self._simplices] + self._simplices = [ + [self._labels[i] for i in s] for s in self._simplices + ] # convert to star if make_star: _to_star(self) elif backend == "cgal": self._simplices = _cgal_triangulate(triang_pts, self._heights) - + # can obtain star more quickly than in QHull by setting height # of origin to be much lower than others # (can't do this in QHull since it sometimes causes errors...) @@ -326,24 +341,26 @@ def __init__(self, origin_mask[self._origin_index] = True heights_masked = np.ma.array(self._heights, mask=origin_mask) - origin_step = max(10, (max(heights_masked[1:]) -\ - min(heights_masked[1:]))) - + origin_step = max( + 10, (max(heights_masked[1:]) - min(heights_masked[1:])) + ) + # reduce height of origin until it's in all simplices while any(self._origin_index not in s for s in self._simplices): self._heights[self._origin_index] -= origin_step - self._simplices = _cgal_triangulate(triang_pts,\ - self._heights) + self._simplices = _cgal_triangulate(triang_pts, self._heights) # map to labels - self._simplices = [[self._labels[i] for i in s] for s\ - in self._simplices] + self._simplices = [ + [self._labels[i] for i in s] for s in self._simplices + ] - else: # Use TOPCOM + else: # Use TOPCOM self._simplices = _topcom_triangulate(triang_pts) # map to labels - self._simplices = [[self._labels[i] for i in s] for s\ - in self._simplices] + self._simplices = [ + [self._labels[i] for i in s] for s in self._simplices + ] # convert to star if make_star: @@ -351,7 +368,7 @@ def __init__(self, # check that the heights uniquely define this triangulation if check_heights and (self._heights is not None): - self.check_heights(verbosity-default_triang) + self.check_heights(verbosity - default_triang) # Make sure that the simplices are sorted self._simplices = sorted([sorted(s) for s in self._simplices]) @@ -359,31 +376,31 @@ def __init__(self, # select the data-type carefully, as the simplices are some of the # biggest data stored in this class... max_label = max(self.poly.labels) - if isinstance(max_label,int): - if max_label<2**8: + if isinstance(max_label, int): + if max_label < 2**8: dtype = np.uint8 - elif max_label<2**16: + elif max_label < 2**16: dtype = np.uint16 - elif max_label<2**32: + elif max_label < 2**32: dtype = np.uint32 else: dtype = np.uint64 - self._simplices = np.array(self._simplices,dtype=dtype) + self._simplices = np.array(self._simplices, dtype=dtype) else: self._simplices = np.array(self._simplices) # also set the heights data structure if self._heights is not None: - self._heights = self._heights/gcd_list(self._heights) + self._heights = self._heights / gcd_list(self._heights) self._heights -= min(self._heights) max_h = max(abs(self._heights)) - if max_h<2**8: + if max_h < 2**8: dtype = np.uint8 - elif max_h<2**16: + elif max_h < 2**16: dtype = np.uint16 - elif max_h<2**32: + elif max_h < 2**32: dtype = np.uint32 else: dtype = np.uint64 @@ -420,15 +437,20 @@ def __repr__(self) -> str: regular_str = ", " regular_str += "regular" if self._is_regular else "irregular" - #star_str = "" - #if self.polytope().is_reflexive(): + # star_str = "" + # if self.polytope().is_reflexive(): star_str = ", " star_str += "star" if self.is_star() else "non-star" - return (f"A " + fine_str + regular_str + star_str +\ - f" triangulation of a {self.dim()}-dimensional " +\ - f"point configuration with {len(self.labels)} points " +\ - f"in ZZ^{self.ambient_dim()}") + return ( + f"A " + + fine_str + + regular_str + + star_str + + f" triangulation of a {self.dim()}-dimensional " + + f"point configuration with {len(self.labels)} points " + + f"in ZZ^{self.ambient_dim()}" + ) def __eq__(self, other: "Triangulation") -> bool: """ @@ -459,8 +481,7 @@ def __eq__(self, other: "Triangulation") -> bool: our_simps = sorted(self.simplices().tolist()) other_simps = sorted(other.simplices().tolist()) - return (self.polytope() == other.polytope() and - our_simps == other_simps) + return self.polytope() == other.polytope() and our_simps == other_simps def __ne__(self, other: "Triangulation") -> bool: """ @@ -483,7 +504,7 @@ def __ne__(self, other: "Triangulation") -> bool: # False ``` """ - return(not self.__eq__(other)) + return not self.__eq__(other) def __hash__(self) -> int: """ @@ -561,7 +582,7 @@ def clear_cache(self, recursive: bool = False) -> None: self._is_regular = None self._is_star = None self._is_valid = None - self._secondary_cone = [None]*2 + self._secondary_cone = [None] * 2 self._sr_ideal = None self._toricvariety = None @@ -585,7 +606,8 @@ def poly(self): The ambient polytope. """ return self._poly - polytope = lambda self:self.poly + + polytope = lambda self: self.poly @property def labels(self): @@ -625,6 +647,7 @@ def dimension(self) -> int: ``` """ return self._dim + # aliases dim = dimension @@ -652,6 +675,7 @@ def ambient_dimension(self) -> int: ``` """ return self.poly.ambient_dim() + # aliases ambient_dim = ambient_dimension @@ -684,12 +708,12 @@ def is_fine(self) -> bool: # calculate the answer N_used_pts = len(set.union(*[set(s) for s in self._simplices])) - self._is_fine = (N_used_pts == len(self.labels)) + self._is_fine = N_used_pts == len(self.labels) # return return self._is_fine - def is_star(self, star_origin = None) -> bool: + def is_star(self, star_origin=None) -> bool: """ **Description:** Returns True if the triangulation is star and False otherwise. The star @@ -718,9 +742,11 @@ def is_star(self, star_origin = None) -> bool: # calculate the answer if self._is_star is None: - #if self._origin_index != -1: + # if self._origin_index != -1: if self.poly._label_origin in self.labels: - self._is_star = all(self.poly._label_origin in s for s in self._simplices) + self._is_star = all( + self.poly._label_origin in s for s in self._simplices + ) else: self._is_star = False @@ -754,11 +780,14 @@ def get_toric_variety(self) -> ToricVariety: # check if the question makes sense if not self._is_fulldim: - raise NotImplementedError("Toric varieties can only be " +\ - "constructed from full-dimensional triangulations.") + raise NotImplementedError( + "Toric varieties can only be " + + "constructed from full-dimensional triangulations." + ) if not self.is_star(): - raise NotImplementedError("Toric varieties can only be " +\ - "constructed from star triangulations.") + raise NotImplementedError( + "Toric varieties can only be " + "constructed from star triangulations." + ) # initialize the answer self._toricvariety = ToricVariety(self) @@ -799,18 +828,21 @@ def get_cy(self, nef_partition: list[tuple[int]] = None) -> "CalabiYau": ``` """ return self.get_toric_variety().get_cy(nef_partition) + # aliases compute_cy = get_cy cy = get_cy # points # ====== - def points(self, - which = None, - optimal: bool = False, - as_poly_indices: bool = False, - as_triang_indices: bool = False, - check_labels: bool = True) -> np.ndarray: + def points( + self, + which=None, + optimal: bool = False, + as_poly_indices: bool = False, + as_triang_indices: bool = False, + check_labels: bool = True, + ) -> np.ndarray: """ **Description:** Returns the points of the triangulation. Note that these are not @@ -847,8 +879,9 @@ def points(self, ``` """ if as_poly_indices and as_triang_indices: - raise ValueError("Both as_poly_indices and as_triang_indices "+\ - "can't be set to True.") + raise ValueError( + "Both as_poly_indices and as_triang_indices " + "can't be set to True." + ) # get the labels of the relevant points if which is None: @@ -856,54 +889,57 @@ def points(self, which = self.labels else: # if which is a single label, wrap it in an iterable - if (not isinstance(which,Iterable)) and (which in self.labels): + if (not isinstance(which, Iterable)) and (which in self.labels): which = [which] # check if the input labels if check_labels: try: if not set(which).issubset(self.labels): - raise ValueError(f"Specified labels ({which}) aren't "\ - "subset of triangulation labels "\ - f"({self.labels})...") + raise ValueError( + f"Specified labels ({which}) aren't " + "subset of triangulation labels " + f"({self.labels})..." + ) except Exception as e: - #print(f"Specified labels, {which}, likely aren't hashable.") + # print(f"Specified labels, {which}, likely aren't hashable.") raise # return if as_triang_indices: if self._labels2inds is None: - self._labels2inds = {v:i for i,v in enumerate(self._labels)} + self._labels2inds = {v: i for i, v in enumerate(self._labels)} return [self._labels2inds[label] for label in which] else: if optimal and (not as_poly_indices): - dim_diff = self.ambient_dim()-self.dim() - if dim_diff>0: + dim_diff = self.ambient_dim() - self.dim() + if dim_diff > 0: # asking for optimal points, where the optimal value may # differ from the entire polytope # calculate the map from labels to optimal points if self._labels2optPts is None: pts_opt = self.points() - pts_opt = lll_reduce(pts_opt-pts_opt[0])[:,dim_diff:] + pts_opt = lll_reduce(pts_opt - pts_opt[0])[:, dim_diff:] self._labels2optPts = dict() - for label,pt in zip(self.labels,pts_opt): + for label, pt in zip(self.labels, pts_opt): self._labels2optPts[label] = tuple(pt) # return the relevant points return np.array([self._labels2optPts[l] for l in which]) # normal case - return self.poly.points(which=which, - optimal=optimal, - as_indices=as_poly_indices) + return self.poly.points( + which=which, optimal=optimal, as_indices=as_poly_indices + ) + # aliases pts = points - def points_to_labels(self, - points: ArrayLike, - is_optimal: bool = False) -> "list | None": + def points_to_labels( + self, points: ArrayLike, is_optimal: bool = False + ) -> "list | None": """ **Description:** Returns the list of labels corresponding to the given points. It also @@ -921,10 +957,12 @@ def points_to_labels(self, """ return self.poly.points_to_labels(points, is_optimal=is_optimal) - def points_to_indices(self, - points: ArrayLike, - is_optimal: bool = False, - as_poly_indices: bool = False) -> "np.ndarray | int": + def points_to_indices( + self, + points: ArrayLike, + is_optimal: bool = False, + as_poly_indices: bool = False, + ) -> "np.ndarray | int": """ **Description:** Returns the list of indices corresponding to the given points. It also @@ -951,25 +989,27 @@ def points_to_indices(self, ``` """ # check for empty input - if len(points)==0: - return np.asarray([],dtype=int) + if len(points) == 0: + return np.asarray([], dtype=int) # map single-point input into list case - single_pt = (len(np.array(points).shape) == 1) + single_pt = len(np.array(points).shape) == 1 if single_pt: points = [points] # grab labels, and then map to indices labels = self.points_to_labels(points, is_optimal=is_optimal) - inds = self.points(which=labels, - as_poly_indices=as_poly_indices, - as_triang_indices=not as_poly_indices) - + inds = self.points( + which=labels, + as_poly_indices=as_poly_indices, + as_triang_indices=not as_poly_indices, + ) + # get/return the indices if single_pt and len(inds): return inds[0] # just return the single index else: - return inds # return a list of indices + return inds # return a list of indices def triangulation_to_polytope_indices(self, inds) -> "np.ndarray | int": """ @@ -986,17 +1026,15 @@ def triangulation_to_polytope_indices(self, inds) -> "np.ndarray | int": the point if only one is given. """ # (NM: remove this... use labels instead...) - return self.points(which=[self._labels[i] for i in inds], - as_poly_indices=True) + return self.points(which=[self._labels[i] for i in inds], as_poly_indices=True) + points_to_poly_indices = triangulation_to_polytope_indices # triangulation # ============= # sanity checks # ------------- - def is_valid(self, - backend: str = None, - verbosity: int = 0) -> bool: + def is_valid(self, backend: str = None, verbosity: int = 0) -> bool: """ **Description:** Returns True if the presumed triangulation meets all requirements to be @@ -1029,8 +1067,8 @@ def is_valid(self, # calculate the answer simps = self.simplices() - #simps = np.array([self.points(s, as_triang_indices=True) for s in simps]) - + # simps = np.array([self.points(s, as_triang_indices=True) for s in simps]) + if simps.shape[0] == 1: # triangulation is trivial self._is_valid = True @@ -1039,13 +1077,15 @@ def is_valid(self, # If the triangulation is presumably regular, then we can check if # heights inside the secondary cone yield the same triangulation. if self.is_regular(backend=backend): - tmp_triang = Triangulation(self.polytope(), - self.labels, - heights=self.heights(), - make_star=False) - - self._is_valid = (self==tmp_triang) - if verbosity>=1: + tmp_triang = Triangulation( + self.polytope(), + self.labels, + heights=self.heights(), + make_star=False, + ) + + self._is_valid = self == tmp_triang + if verbosity >= 1: msg = f"By regularity, returning valid={self._is_valid}" print(msg) return self._is_valid @@ -1054,10 +1094,16 @@ def is_valid(self, # triangulation. This can be quite slow for large polytopes. # append a 1 to each point - pts = {l:pt for l,pt in zip(self.labels,self.points(optimal=True))} - pts_ext = {l:list(pts[l])+[1,] for l in self.labels} + pts = {l: pt for l, pt in zip(self.labels, self.points(optimal=True))} + pts_ext = { + l: list(pts[l]) + + [ + 1, + ] + for l in self.labels + } pts_all = np.array(list(pts.values())) - #pts_ext = np.array([list(pt)+[1,] for pt in pts]) + # pts_ext = np.array([list(pt)+[1,] for pt in pts]) # We first check if the volumes add up to the volume of the polytope v = 0 @@ -1066,27 +1112,29 @@ def is_valid(self, if tmp_v == 0: self._is_valid = False - if verbosity>=1: + if verbosity >= 1: msg = f"By simp volume, returning valid={self._is_valid}" print(msg) return self._is_valid v += tmp_v - poly_vol = int(round(ConvexHull(pts_all).volume*math.factorial(self._dim))) + poly_vol = int(round(ConvexHull(pts_all).volume * math.factorial(self._dim))) if v != poly_vol: self._is_valid = False - if verbosity>=1: - msg = f"Simp volume ({v}) != poly volume ({poly_vol})... " +\ - f"returning valid={self._is_valid}" + if verbosity >= 1: + msg = ( + f"Simp volume ({v}) != poly volume ({poly_vol})... " + + f"returning valid={self._is_valid}" + ) print(msg) return self._is_valid # Finally, check if simplices have full-dimensional intersections - for i,s1 in enumerate(simps): + for i, s1 in enumerate(simps): pts_1 = [pts_ext[i] for i in s1] - for s2 in simps[i+1:]: + for s2 in simps[i + 1 :]: pts_2 = [pts_ext[i] for i in s2] inters = Cone(pts_1).intersection(Cone(pts_2)) @@ -1116,31 +1164,36 @@ def check_heights(self, verbosity: int = 1) -> bool: """ # find hyperplanes that we are within eps of wall eps = 1e-6 - hyp_dist = np.matmul(self.secondary_cone().hyperplanes(),\ - self._heights) - not_interior = hyp_dist1: - print(f"Triangulation: height-vector is within {eps}"+\ - " of a wall of the secondary cone... heights "+\ - "likely don't define a unique triangulation " +\ - "(common for Delaunay)...") - print("Will recalculate more appropriate heights " +\ - "for the (semi-arbitrarily chosen) " +\ - "triangulation...") + if verbosity > 1: + print( + f"Triangulation: height-vector is within {eps}" + + " of a wall of the secondary cone... heights " + + "likely don't define a unique triangulation " + + "(common for Delaunay)..." + ) + print( + "Will recalculate more appropriate heights " + + "for the (semi-arbitrarily chosen) " + + "triangulation..." + ) # main method # ----------- - def simplices(self, - on_faces_dim: int = None, - on_faces_codim: int = None, - split_by_face: bool = False, - as_np_array: bool = True, - as_indices: bool = False) -> "set | np.ndarray": + def simplices( + self, + on_faces_dim: int = None, + on_faces_codim: int = None, + split_by_face: bool = False, + as_np_array: bool = True, + as_indices: bool = False, + ) -> "set | np.ndarray": """ **Description:** Returns the simplices of the triangulation. It also has the option of @@ -1188,7 +1241,7 @@ def simplices(self, # cast to indices if as_indices: - l2i = {l:i for i,l in enumerate(self.labels)} + l2i = {l: i for i, l in enumerate(self.labels)} out = [[l2i[l] for l in s] for s in out] if as_np_array: @@ -1198,9 +1251,9 @@ def simplices(self, elif on_faces_dim is not None: faces_dim = on_faces_dim else: - faces_dim = self.dim()-on_faces_codim + faces_dim = self.dim() - on_faces_codim - if faces_dim<0 or faces_dim>self.dim(): + if faces_dim < 0 or faces_dim > self.dim(): raise ValueError("Invalid face dimension.") # restrict simplices to faces @@ -1212,13 +1265,13 @@ def simplices(self, for face in self.polytope().faces(faces_dim): face_labels.append(frozenset(face.labels)) - # actually restrict + # actually restrict restricted = [] for f in face_labels: restricted.append(set()) for s in full_simp: inters = f & s - if len(inters) == faces_dim+1: + if len(inters) == faces_dim + 1: restricted[-1].add(inters) self._restricted_simplices[faces_dim] = restricted @@ -1228,9 +1281,8 @@ def simplices(self, # cast to indices if as_indices: - l2i = {l:i for i,l in enumerate(self.labels)} - out = [[frozenset([l2i[l] for l in s]) for s in face]\ - for face in out] + l2i = {l: i for i, l in enumerate(self.labels)} + out = [[frozenset([l2i[l] for l in s]) for s in face] for face in out] if split_by_face: if as_np_array: @@ -1239,16 +1291,19 @@ def simplices(self, out = set().union(*out) if as_np_array: return np.array(sorted(sorted(s) for s in out)) - + return out + # aliases simps = simplices - def restrict(self, - restrict_to: ["PolytopeFace"] = None, - restrict_dim: int = 2, - as_poly: bool = False, - verbosity: int = 0): + def restrict( + self, + restrict_to: ["PolytopeFace"] = None, + restrict_dim: int = 2, + as_poly: bool = False, + verbosity: int = 0, + ): """ **Description:** Restrict the triangulation to some face(s). @@ -1270,10 +1325,15 @@ def restrict(self, if isinstance(restrict_to, Iterable): # recursivesly call for each face given - return [self.restrict(restrict_to=f, - as_face_inds=as_face_inds, - as_poly=as_poly, - verbosity=verbosity) for f in restrict_to] + return [ + self.restrict( + restrict_to=f, + as_face_inds=as_face_inds, + as_poly=as_poly, + verbosity=verbosity, + ) + for f in restrict_to + ] # if above checks failed, then single face was input... @@ -1290,13 +1350,14 @@ def restrict(self, for simp in self.simplices(): restricted = label_set.intersection(simp) - if len(restricted)==(dim+1): + if len(restricted) == (dim + 1): # full dimension restriction face_simps.add(tuple(restricted)) if as_poly: - restricted = restrict_to.triangulate(simplices=face_simps, - verbosity=verbosity-1) + restricted = restrict_to.triangulate( + simplices=face_simps, verbosity=verbosity - 1 + ) restricted._ambient_triangulation = self return restricted else: @@ -1304,12 +1365,14 @@ def restrict(self, # regularity # ---------- - def secondary_cone(self, - backend: str = None, - include_points_not_in_triangulation: bool = True, - as_cone: bool = True, - on_faces_dim: int = None, - use_cache: bool = True) -> Cone: + def secondary_cone( + self, + backend: str = None, + include_points_not_in_triangulation: bool = True, + as_cone: bool = True, + on_faces_dim: int = None, + use_cache: bool = True, + ) -> Cone: """ **Description:** Computes the (hyperplanes defining the) secondary cone of the @@ -1382,11 +1445,16 @@ def secondary_cone(self, else: backend = "topcom" - if (backend == "native" and (not self.is_fine()) and\ - include_points_not_in_triangulation): - warnings.warn("Native backend is not supported when including " - "points that are not in the triangulation. Using " - "TOPCOM...") + if ( + backend == "native" + and (not self.is_fine()) + and include_points_not_in_triangulation + ): + warnings.warn( + "Native backend is not supported when including " + "points that are not in the triangulation. Using " + "TOPCOM..." + ) backend = "topcom" # set on_faces_dim @@ -1396,18 +1464,20 @@ def secondary_cone(self, # compute the cone # ---------------- # want the secondary cone of the N-skeleton for N bool: # return return self._is_regular - def heights(self, - integral: bool = False, - backend: str = None) -> np.ndarray: + def heights(self, integral: bool = False, backend: str = None) -> np.ndarray: """ **Description:** Returns the a height vector if the triangulation is regular. An @@ -1616,10 +1685,12 @@ def heights(self, heights_out = self._heights.copy() # convert to integral heights, if desired - if integral and (heights_out.dtype==float): - warnings.warn("Potential rounding errors... better to" +\ - "solve LP problem in this case...") - heights_out = np.rint(heights_out/gcd_list(heights_out)) + if integral and (heights_out.dtype == float): + warnings.warn( + "Potential rounding errors... better to" + + "solve LP problem in this case..." + ) + heights_out = np.rint(heights_out / gcd_list(heights_out)) if integral: return heights_out.astype(int) @@ -1628,23 +1699,24 @@ def heights(self, # need to calculate the heights Npts = len(self.labels) - if (self._simplices.shape[0]==1) and (self._simplices.shape[1]==Npts): + if (self._simplices.shape[0] == 1) and (self._simplices.shape[1] == Npts): # If the triangulation is trivial we just return a vector of zeros self._heights = np.zeros(Npts, dtype=(int if integral else float)) else: # Otherwise we find a point in the secondary cone C = self.secondary_cone(include_points_not_in_triangulation=True) - self._heights = C.find_interior_point(integral=integral, - backend=backend) + self._heights = C.find_interior_point(integral=integral, backend=backend) return self._heights.copy() # symmetries # ========== - def automorphism_orbit(self, - automorphism: "int | ArrayLike" = None, - on_faces_dim: int = None, - on_faces_codim: int = None) -> np.ndarray: + def automorphism_orbit( + self, + automorphism: "int | ArrayLike" = None, + on_faces_dim: int = None, + on_faces_codim: int = None, + ) -> np.ndarray: """ **Description:** Returns all of the triangulations of the polytope that can be obtained @@ -1695,8 +1767,10 @@ def automorphism_orbit(self, """ # sanity checks if not self._is_fulldim: - raise NotImplementedError("Automorphisms can only be computed " +\ - "for full-dimensional point configurations.") + raise NotImplementedError( + "Automorphisms can only be computed " + + "for full-dimensional point configurations." + ) # input parsing if (on_faces_dim is None) and (on_faces_codim is None): @@ -1704,12 +1778,12 @@ def automorphism_orbit(self, elif on_faces_dim is not None: faces_dim = on_faces_dim else: - faces_dim = self.dim()-on_faces_codim + faces_dim = self.dim() - on_faces_codim if automorphism is None: orbit_id = (None, faces_dim) else: - if isinstance(automorphism,Iterable): + if isinstance(automorphism, Iterable): orbit_id = (tuple(automorphism), faces_dim) else: orbit_id = ((automorphism,), faces_dim) @@ -1726,17 +1800,17 @@ def automorphism_orbit(self, # the triangulation point configuration good_autos = [] for i in range(len(autos)): - for j,k in autos[i].items(): + for j, k in autos[i].items(): # user-input 'allowed' automorphims - if (orbit_id[0] is not None) and (i>0)\ - and (i not in orbit_id[0]): + if (orbit_id[0] is not None) and (i > 0) and (i not in orbit_id[0]): # non-trivial automorphism that isn't specifically allowed break # check if jth and kth points are either both in the # triangulation, or both not in it - if (self.poly.labels[j] in self.labels) !=\ - (self.poly.labels[k] in self.labels): + if (self.poly.labels[j] in self.labels) != ( + self.poly.labels[k] in self.labels + ): # oops! One pt is in the triangulation while other isn't! break else: @@ -1755,25 +1829,28 @@ def automorphism_orbit(self, # it's a 'good' automorphism temp = {} - for j,jj in autos[i].items(): - if (self.poly.labels[j] in self.labels) and\ - (self.poly.labels[jj] in self.labels): + for j, jj in autos[i].items(): + if (self.poly.labels[j] in self.labels) and ( + self.poly.labels[jj] in self.labels + ): tmp_labels = [self.poly.labels[j], self.poly.labels[jj]] - idx_j,idx_jj = self.points(tmp_labels, - as_triang_indices=True, - check_labels=False) + idx_j, idx_jj = self.points( + tmp_labels, as_triang_indices=True, check_labels=False + ) temp[idx_j] = idx_jj autos[i] = temp # define helper function - apply_auto = lambda auto: tuple(sorted(\ - tuple(sorted( [auto[i] for i in s] )) for s in simps )) + apply_auto = lambda auto: tuple( + sorted(tuple(sorted([auto[i] for i in s])) for s in simps) + ) # apply the automorphisms orbit = set() - for j,a in enumerate(autos): + for j, a in enumerate(autos): # skip if it is a 'bad' automorphism - if a is None: continue + if a is None: + continue # it's a 'good' automorphism orbit.add(apply_auto(a)) @@ -1799,11 +1876,13 @@ def automorphism_orbit(self, self._automorphism_orbit[orbit_id] = np.array(sorted(orbit)) return np.array(self._automorphism_orbit[orbit_id]) - def is_equivalent(self, - other: "Triangulation", - use_automorphisms: bool = True, - on_faces_dim: int = None, - on_faces_codim: int = None) -> bool: + def is_equivalent( + self, + other: "Triangulation", + use_automorphisms: bool = True, + on_faces_dim: int = None, + on_faces_codim: int = None, + ) -> bool: """ **Description:** Compares two triangulations with or without allowing automorphism @@ -1845,29 +1924,33 @@ def is_equivalent(self, # check via automorphisms if self._is_fulldim and use_automorphisms: - orbit1 = self.automorphism_orbit(on_faces_dim=on_faces_dim,\ - on_faces_codim=on_faces_codim) - orbit2 = other.automorphism_orbit(on_faces_dim=on_faces_dim,\ - on_faces_codim=on_faces_codim) + orbit1 = self.automorphism_orbit( + on_faces_dim=on_faces_dim, on_faces_codim=on_faces_codim + ) + orbit2 = other.automorphism_orbit( + on_faces_dim=on_faces_dim, on_faces_codim=on_faces_codim + ) - return (orbit1.shape==orbit2.shape) and all((orbit1==orbit2).flat) + return (orbit1.shape == orbit2.shape) and all((orbit1 == orbit2).flat) # check via simplices - simp1 = self.simplices(on_faces_dim=on_faces_dim,\ - on_faces_codim=on_faces_codim) - simp2 = other.simplices(on_faces_dim=on_faces_dim,\ - on_faces_codim=on_faces_codim) + simp1 = self.simplices(on_faces_dim=on_faces_dim, on_faces_codim=on_faces_codim) + simp2 = other.simplices( + on_faces_dim=on_faces_dim, on_faces_codim=on_faces_codim + ) - return (simp1.shape==simp2.shape) and all((simp1==simp2).flat) + return (simp1.shape == simp2.shape) and all((simp1 == simp2).flat) # flips # ===== - def neighbor_triangulations(self, - only_fine: bool = False, - only_regular: bool = False, - only_star: bool = False, - backend: str = None, - verbose : bool = False)->list["Triangulation"]: + def neighbor_triangulations( + self, + only_fine: bool = False, + only_regular: bool = False, + only_star: bool = False, + backend: str = None, + verbose: bool = False, + ) -> list["Triangulation"]: """ **Description:** Returns the list of triangulations that differ by one bistellar flip @@ -1902,21 +1985,23 @@ def neighbor_triangulations(self, ``` """ # check for topcom bug - if len(self.simplices())==1: - warnings.warn("Triangulation.neighbor_triangulations called " + \ - "for trivial triangulation (1 simplex)... " + \ - "Returning []! Fix TOPCOM!") + if len(self.simplices()) == 1: + warnings.warn( + "Triangulation.neighbor_triangulations called " + + "for trivial triangulation (1 simplex)... " + + "Returning []! Fix TOPCOM!" + ) return [] # optimized method for 2D fine neighbors - if self.is_fine() and (self.dim()==2) and only_fine: + if self.is_fine() and (self.dim() == 2) and only_fine: return self._fine_neighbors_2d() # prep TOPCOM input - l2i = {l:i for i,l in enumerate(self.labels)} - pts_str = str([list(pt)+[1] for pt in self.points(optimal=True)]) + l2i = {l: i for i, l in enumerate(self.labels)} + pts_str = str([list(pt) + [1] for pt in self.points(optimal=True)]) triang_str = str([[l2i[l] for l in s] for s in self._simplices]) - triang_str = triang_str.replace("[","{").replace("]","}") + triang_str = triang_str.replace("[", "{").replace("]", "}") flips_str = "(-1)" topcom_input = pts_str + "[]" + triang_str + flips_str @@ -1924,13 +2009,16 @@ def neighbor_triangulations(self, # prep TOPCOM topcom_bin = config.topcom_path + "topcom-points2flips" if verbose: - cmd = (topcom_bin,"-v") + cmd = (topcom_bin, "-v") else: cmd = (topcom_bin,) - topcom = subprocess.Popen(cmd, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True) + topcom = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) # do the work and read output topcom_res, topcom_err = topcom.communicate(input=topcom_input) @@ -1946,22 +2034,23 @@ def neighbor_triangulations(self, if verbose: print(topcom_err) - if len(topcom_res)==0: + if len(topcom_res) == 0: return [] - triangs_list = [ast.literal_eval(r) for r in\ - topcom_res.strip().split("\n")] + triangs_list = [ast.literal_eval(r) for r in topcom_res.strip().split("\n")] # parse the outputs triangs = [] for t in triangs_list: - if not all(len(s)==self.dim()+1 for s in t): + if not all(len(s) == self.dim() + 1 for s in t): continue # construct and check triangulation - tri = Triangulation(self.poly, - self.labels, - simplices=[[self.labels[i] for i in s] for s in t], - check_input_simplices=False) + tri = Triangulation( + self.poly, + self.labels, + simplices=[[self.labels[i] for i in s] for s in t], + check_input_simplices=False, + ) if only_fine and (not tri.is_fine()): continue if only_star and (not tri.is_star()): @@ -1972,16 +2061,19 @@ def neighbor_triangulations(self, # keep it :) triangs.append(tri) return triangs + # aliases neighbors = neighbor_triangulations - def random_flips(self, - N: int, - only_fine: bool = None, - only_regular: bool = None, - only_star: bool = None, - backend: str = None, - seed: int = None) -> "Triangulation": + def random_flips( + self, + N: int, + only_fine: bool = None, + only_regular: bool = None, + only_star: bool = None, + backend: str = None, + seed: int = None, + ) -> "Triangulation": """ **Description:** Returns a triangulation obtained by performing N random bistellar @@ -2029,9 +2121,9 @@ def random_flips(self, # take random flips curr_triang = self for n in range(N): - neighbors = curr_triang.neighbor_triangulations(only_fine=False,\ - only_regular=False,\ - only_star=False) + neighbors = curr_triang.neighbor_triangulations( + only_fine=False, only_regular=False, only_star=False + ) np.random.shuffle(neighbors) for t in neighbors: @@ -2051,10 +2143,12 @@ def random_flips(self, return curr_triang - def _fine_neighbors_2d(self, - only_regular: bool = False, - only_star: bool = False, - backend: str = None)->list["Triangulation"]: + def _fine_neighbors_2d( + self, + only_regular: bool = False, + only_star: bool = False, + backend: str = None, + ) -> list["Triangulation"]: """ **Description:** An optimized variant of neighbor_triangulations for triangulations that @@ -2082,33 +2176,36 @@ def _fine_neighbors_2d(self, # for each pair of simplices for i, s1 in enumerate(simps_set): - for _j, s2 in enumerate(simps_set[i+1:]): - j = i+1+_j - + for _j, s2 in enumerate(simps_set[i + 1 :]): + j = i + 1 + _j + # check if they form a quadrilateral # (i.e., if they intersect along an edge) - inter = s1&s2 - if len(inter)!=2: + inter = s1 & s2 + if len(inter) != 2: continue - + # (and if the edge is 'internal') - other = s1.union(s2)-inter + other = s1.union(s2) - inter pts_inter = self.points(inter, check_labels=False) pts_other = self.points(other, check_labels=False) if (sum(pts_inter) != sum(pts_other)).any(): continue - + # flip the inner diagonal - flipped = list(map(lambda p: sorted(other|{p}), inter)) + flipped = list(map(lambda p: sorted(other | {p}), inter)) new_simps = self._simplices.copy() new_simps[i] = flipped[0] new_simps[j] = flipped[1] # construct the triangulation - tri = Triangulation(self.poly, self.labels,\ - simplices=new_simps,\ - check_input_simplices=False) + tri = Triangulation( + self.poly, + self.labels, + simplices=new_simps, + check_input_simplices=False, + ) # check the triangulation if only_star and (not tri.is_star()): @@ -2150,9 +2247,14 @@ def gkz_phi(self) -> np.ndarray: return np.array(self._gkz_phi) # calculate the answer - pts_ext = {l:list(pt)+[1,] for l,pt in\ - zip(self.labels, self.points(optimal=True))} - l2i = {l:i for i,l in enumerate(self.labels)} + pts_ext = { + l: list(pt) + + [ + 1, + ] + for l, pt in zip(self.labels, self.points(optimal=True)) + } + l2i = {l: i for i, l in enumerate(self.labels)} phi = np.zeros(len(pts_ext), dtype=int) for s in self._simplices: @@ -2205,43 +2307,46 @@ def sr_ideal(self) -> tuple: # check if we can answer if not self.is_star() or not self._is_fulldim: - raise NotImplementedError("SR ideals can only be computed for "+\ - "full-dimensional star triangulations.") + raise NotImplementedError( + "SR ideals can only be computed for " + + "full-dimensional star triangulations." + ) # prep-work labels = set(self.labels) - {self.poly._label_origin} simplices = [labels.intersection(s) for s in self.simplices()] simplex_tuples = [] - for dd in range(1,self.dim()+1): + for dd in range(1, self.dim() + 1): simplex_tuples.append(set()) for s in simplices: - simplex_tuples[-1].update(frozenset(tup) for tup in\ - itertools.combinations(s, dd)) + simplex_tuples[-1].update( + frozenset(tup) for tup in itertools.combinations(s, dd) + ) # calculate the SR ideal SR_ideal, checked = set(), set() - for i in range(len(simplex_tuples)-1): + for i in range(len(simplex_tuples) - 1): for tup in simplex_tuples[i]: for j in labels: k = tup.union((j,)) # skip if already checked - if (k in checked) or (len(k)!=len(tup)+1): + if (k in checked) or (len(k) != len(tup) + 1): continue else: checked.add(k) - if k in simplex_tuples[i+1]: + if k in simplex_tuples[i + 1]: continue # check it in_SR = False - for order in range(1, i+1): + for order in range(1, i + 1): for t in itertools.combinations(tup, order): - if frozenset(t+(j,)) in SR_ideal: + if frozenset(t + (j,)) in SR_ideal: in_SR = True break else: @@ -2250,7 +2355,7 @@ def sr_ideal(self) -> tuple: continue # there was a t at this order such that - # frozenset(t+(j,)) was in SR_ideal for some + # frozenset(t+(j,)) was in SR_ideal for some break else: # frozenset(t+(j,)) was not in SR_ideal for any order @@ -2258,9 +2363,10 @@ def sr_ideal(self) -> tuple: # return self._sr_ideal = [tuple(sorted(s)) for s in SR_ideal] - self._sr_ideal = tuple(sorted(self._sr_ideal, key=lambda x:(len(x),x))) + self._sr_ideal = tuple(sorted(self._sr_ideal, key=lambda x: (len(x), x))) return self._sr_ideal + def _to_star(triang: Triangulation) -> np.ndarray: """ **Description:** @@ -2303,6 +2409,7 @@ def _to_star(triang: Triangulation) -> np.ndarray: # update triang triang._simplices = np.array(sorted([sorted(s) for s in star_triang])) + def _qhull_triangulate(points: ArrayLike, heights: ArrayLike) -> np.ndarray: """ **Description:** @@ -2331,19 +2438,22 @@ def _qhull_triangulate(points: ArrayLike, heights: ArrayLike) -> np.ndarray: # points in ZZ^4 ``` """ - lifted_points = [tuple(points[i]) + (heights[i],) for i in\ - range(len(points))] + lifted_points = [tuple(points[i]) + (heights[i],) for i in range(len(points))] hull = ConvexHull(lifted_points) # We first pick the lower facets of the convex hull - low_fac = [hull.simplices[n] for n,nn in enumerate(hull.equations)\ - if nn[-2] < 0] # The -2 component is the lifting dimension + low_fac = [ + hull.simplices[n] for n, nn in enumerate(hull.equations) if nn[-2] < 0 + ] # The -2 component is the lifting dimension # Then we only take the faces that project to full-dimensional simplices # in the original point configuration lifted_points = [pt[:-1] + (1,) for pt in lifted_points] - simp = [s for s in low_fac if int(round(np.linalg.det([lifted_points[i]\ - for i in s]))) != 0] + simp = [ + s + for s in low_fac + if int(round(np.linalg.det([lifted_points[i] for i in s]))) != 0 + ] return np.array(sorted([sorted(s) for s in simp])) @@ -2384,14 +2494,19 @@ def _cgal_triangulate(points: ArrayLike, heights: ArrayLike) -> np.ndarray: # pass the command to CGAL cgal_bin = config.cgal_path - cgal_bin += (f"/cgal-triangulate-{dim}d" if dim in (2,3,4,5)\ - else "cgal-triangulate") - cgal = subprocess.Popen((cgal_bin,), stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - universal_newlines=True) + cgal_bin += ( + f"/cgal-triangulate-{dim}d" if dim in (2, 3, 4, 5) else "cgal-triangulate" + ) + cgal = subprocess.Popen( + (cgal_bin,), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) # read/parse outputs - cgal_res, cgal_err = cgal.communicate(input=pts_str+heights_str) + cgal_res, cgal_err = cgal.communicate(input=pts_str + heights_str) if cgal_err != "": raise RuntimeError(f"CGAL error: {cgal_err}") @@ -2434,32 +2549,40 @@ def _topcom_triangulate(points: ArrayLike) -> np.ndarray: """ # prep topcom_bin = config.topcom_path + "/topcom-points2finetriang" - topcom = subprocess.Popen((topcom_bin, "--regular"), stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - universal_newlines=True) + topcom = subprocess.Popen( + (topcom_bin, "--regular"), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) # do the work - pts_str = str([list(pt)+[1] for pt in points]) - topcom_res, topcom_err = topcom.communicate(input=pts_str+"[]") + pts_str = str([list(pt) + [1] for pt in points]) + topcom_res, topcom_err = topcom.communicate(input=pts_str + "[]") # parse the output try: simp = ast.literal_eval(topcom_res.replace("{", "[").replace("}", "]")) except: - raise RuntimeError("Error: Failed to parse TOPCOM output. " - f"\nstdout: {topcom_res} \nstderr: {topcom_err}") + raise RuntimeError( + "Error: Failed to parse TOPCOM output. " + f"\nstdout: {topcom_res} \nstderr: {topcom_err}" + ) return np.array(sorted([sorted(s) for s in simp])) -def all_triangulations(poly: "Polytope", - pts: ArrayLike, - only_fine: bool = False, - only_regular: bool = False, - only_star: bool = False, - star_origin: int = None, - backend: str = None, - raw_output: bool = False) -> "generator[Triangulation]": +def all_triangulations( + poly: "Polytope", + pts: ArrayLike, + only_fine: bool = False, + only_regular: bool = False, + only_star: bool = False, + star_origin: int = None, + backend: str = None, + raw_output: bool = False, +) -> "generator[Triangulation]": """ **Description:** Computes all triangulations of the input point configuration using TOPCOM. @@ -2518,8 +2641,10 @@ def all_triangulations(poly: "Polytope", raise ValueError("List of points cannot be empty.") if only_star and star_origin is None: - raise ValueError("The star_origin parameter must be specified when " - "restricting to star triangulations.") + raise ValueError( + "The star_origin parameter must be specified when " + "restricting to star triangulations." + ) # ensure points are appropriately sorted (for Triangulation inputs) triang_pts = poly.points(which=pts) @@ -2529,11 +2654,11 @@ def all_triangulations(poly: "Polytope", # if not full-dimenstional, find better representation # (only performs affine transformation, so can treat the new points as if # they were the original ones) - dim = np.linalg.matrix_rank([tuple(pt)+(1,) for pt in triang_pts])-1 + dim = np.linalg.matrix_rank([tuple(pt) + (1,) for pt in triang_pts]) - 1 if dim == triang_pts.shape[1]: optimal_pts = triang_pts else: - optimal_pts = lll_reduce([pt-triang_pts[0] for pt in triang_pts])[:,-dim:] + optimal_pts = lll_reduce([pt - triang_pts[0] for pt in triang_pts])[:, -dim:] # prep for TOPCOM topcom_bin = config.topcom_path @@ -2542,50 +2667,69 @@ def all_triangulations(poly: "Polytope", else: topcom_bin += "topcom-points2alltriangs" - reg_arg = ("--regular",) if backend=="topcom" and only_regular else () - topcom = subprocess.Popen((topcom_bin,)+reg_arg, - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, universal_newlines=True) + reg_arg = ("--regular",) if backend == "topcom" and only_regular else () + topcom = subprocess.Popen( + (topcom_bin,) + reg_arg, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) # do the work - pts_str = str([list(pt)+[1] for pt in optimal_pts]) - topcom_res, topcom_err = topcom.communicate(input=pts_str+"[]") + pts_str = str([list(pt) + [1] for pt in optimal_pts]) + topcom_res, topcom_err = topcom.communicate(input=pts_str + "[]") - #parse the output + # parse the output try: - triangs = [ast.literal_eval("["+ t.replace("{","[").replace("}","]") +\ - "]") for t in re.findall(r"\{([^\:]*)\}", topcom_res)] + triangs = [ + ast.literal_eval("[" + t.replace("{", "[").replace("}", "]") + "]") + for t in re.findall(r"\{([^\:]*)\}", topcom_res) + ] except: - raise RuntimeError("Error: Failed to parse TOPCOM output. " - f"\nstdout: {topcom_res} \nstderr: {topcom_err}") + raise RuntimeError( + "Error: Failed to parse TOPCOM output. " + f"\nstdout: {topcom_res} \nstderr: {topcom_err}" + ) # map the triangulations to labels triangs = [[pts[i] for i in s] for s in triangs] # sort the triangs - srt_triangs = [np.array(sorted([sorted(s) for s in t])) for t in triangs - if (not only_star or all(star_origin in ss for ss in t))] + srt_triangs = [ + np.array(sorted([sorted(s) for s in t])) + for t in triangs + if (not only_star or all(star_origin in ss for ss in t)) + ] # return the output for t in srt_triangs: if raw_output: yield t continue - tri = Triangulation(poly, pts, simplices=t, make_star=False, - check_input_simplices=False) + tri = Triangulation( + poly, + pts, + simplices=t, + make_star=False, + check_input_simplices=False, + ) if not only_regular or tri.is_regular(backend=backend): yield tri -def random_triangulations_fast_generator(poly: "Polytope", - pts: ArrayLike, - N: int = None, - c: float = 0.2, - max_retries: int = 500, - make_star: bool = False, - only_fine: bool = True, - backend: str = "cgal", - seed: int = None, - verbosity: int = 0) -> "generator[Triangulation]": + +def random_triangulations_fast_generator( + poly: "Polytope", + pts: ArrayLike, + N: int = None, + c: float = 0.2, + max_retries: int = 500, + make_star: bool = False, + only_fine: bool = True, + backend: str = "cgal", + seed: int = None, + verbosity: int = 0, +) -> "generator[Triangulation]": """ Constructs pseudorandom regular (optionally fine and star) triangulations of a given point set. This is done by picking random heights around the @@ -2657,19 +2801,28 @@ def random_triangulations_fast_generator(poly: "Polytope", n_retries = 0 while True: if n_retries >= max_retries: - if verbosity>0: - print("random_triangulations_fast_generator: Hit max_retries... returning") + if verbosity > 0: + print( + "random_triangulations_fast_generator: Hit max_retries... returning" + ) return if (N is not None) and (len(triang_hashes) >= N): - if verbosity>1: - print("random_triangulations_fast_generator: Generated enough triangulations... returning") + if verbosity > 1: + print( + "random_triangulations_fast_generator: Generated enough triangulations... returning" + ) return # generate random heights, make the triangulation - heights= [pt.dot(pt) + np.random.normal(0,c) for pt in poly.points(which=pts)] - t = Triangulation(poly, pts, heights=heights, - make_star=make_star, backend=backend, - check_heights=False) + heights = [pt.dot(pt) + np.random.normal(0, c) for pt in poly.points(which=pts)] + t = Triangulation( + poly, + pts, + heights=heights, + make_star=make_star, + backend=backend, + check_heights=False, + ) # check if it's good if only_fine and (not t.is_fine()): @@ -2677,7 +2830,7 @@ def random_triangulations_fast_generator(poly: "Polytope", continue # check that the heights aren't on a wall of the secondary cone - t.check_heights(verbosity-1) + t.check_heights(verbosity - 1) h = hash(t) if h in triang_hashes: @@ -2689,19 +2842,22 @@ def random_triangulations_fast_generator(poly: "Polytope", n_retries = 0 yield t -def random_triangulations_fair_generator(poly: "Polytope", - pts: ArrayLike, - N: int = None, - n_walk: int = 10, - n_flip: int = 10, - initial_walk_steps: int = 20, - walk_step_size: float = 1e-2, - max_steps_to_wall: int = 10, - fine_tune_steps: int = 8, - max_retries: int = 50, - make_star: bool = False, - backend: str = "cgal", - seed: int = None) -> "generator[Triangulation]": + +def random_triangulations_fair_generator( + poly: "Polytope", + pts: ArrayLike, + N: int = None, + n_walk: int = 10, + n_flip: int = 10, + initial_walk_steps: int = 20, + walk_step_size: float = 1e-2, + max_steps_to_wall: int = 10, + fine_tune_steps: int = 8, + max_retries: int = 50, + make_star: bool = False, + backend: str = "cgal", + seed: int = None, +) -> "generator[Triangulation]": """ **Description:** Constructs pseudorandom regular (optionally star) triangulations of a given @@ -2717,7 +2873,7 @@ def random_triangulations_fair_generator(poly: "Polytope", :::note notes - This function is not intended to be called by the end user. Instead, it - is used by the + is used by the [`random_triangulations_fair`](./polytope#random_triangulations_fair) function of the [`Polytope`](./polytope) class. - This function is designed mainly for large polytopes where sampling @@ -2786,36 +2942,36 @@ def random_triangulations_fair_generator(poly: "Polytope", traing_pts = poly.points(which=pts) num_points = len(pts) - dim = np.linalg.matrix_rank([tuple(pt)+(1,) for pt in traing_pts])-1 + dim = np.linalg.matrix_rank([tuple(pt) + (1,) for pt in traing_pts]) - 1 if dim != traing_pts.shape[1]: raise Exception("Point configuration must be full-dimensional.") if seed is not None: np.random.seed(seed) - + # Obtain random Delaunay triangulation by picking random point as origin - rand_ind = np.random.randint(0,len(pts)) - points_shifted = [p-traing_pts[rand_ind] for p in traing_pts] + rand_ind = np.random.randint(0, len(pts)) + points_shifted = [p - traing_pts[rand_ind] for p in traing_pts] - delaunay_heights = [walk_step_size*(np.dot(p,p)) for p in points_shifted] + delaunay_heights = [walk_step_size * (np.dot(p, p)) for p in points_shifted] start_pt = delaunay_heights old_pt = start_pt - step_size = walk_step_size*np.mean(delaunay_heights) - + step_size = walk_step_size * np.mean(delaunay_heights) + # initialize for MCMC - step_ctr = 0 # total # of steps taken - step_per_tri_ctr = 0 # # of steps taken for given triangulation - + step_ctr = 0 # total # of steps taken + step_per_tri_ctr = 0 # # of steps taken for given triangulation + n_retries = 0 triang_hashes = set() # do the work while True: # check if we're done - if n_retries>=max_retries: + if n_retries >= max_retries: break - if (N is not None) and (len(triang_hashes)>N): + if (N is not None) and (len(triang_hashes) > N): break # find a wall @@ -2830,10 +2986,15 @@ def random_triangulations_fair_generator(poly: "Polytope", # take steps for __ in range(max_steps_to_wall): - new_pt = in_pt + random_dir*step_size - temp_tri = Triangulation(poly, pts, heights=new_pt, - make_star=False, backend=backend, - verbosity=0) + new_pt = in_pt + random_dir * step_size + temp_tri = Triangulation( + poly, + pts, + heights=new_pt, + make_star=False, + backend=backend, + verbosity=0, + ) # check triang if temp_tri.is_fine(): @@ -2854,11 +3015,16 @@ def random_triangulations_fair_generator(poly: "Polytope", # Find the location of the boundary fine_tune_ctr = 0 in_pt_found = False - while (fine_tune_ctrinitial_walk_steps) and (step_per_tri_ctr>=n_walk): - flip_seed_tri =Triangulation(poly, pts, heights=new_pt, - make_star=make_star, backend=backend, - verbosity=0) + if (step_ctr > initial_walk_steps) and (step_per_tri_ctr >= n_walk): + flip_seed_tri = Triangulation( + poly, + pts, + heights=new_pt, + make_star=make_star, + backend=backend, + verbosity=0, + ) # take flips if n_flip > 0: - temp_tri = flip_seed_tri.random_flips(n_flip, only_fine=True, - only_regular=True, - only_star=True) + temp_tri = flip_seed_tri.random_flips( + n_flip, only_fine=True, only_regular=True, only_star=True + ) else: temp_tri = flip_seed_tri @@ -2902,7 +3072,7 @@ def random_triangulations_fair_generator(poly: "Polytope", yield temp_tri # update old point - old_pt = new_pt/np.linalg.norm(new_pt) + old_pt = new_pt / np.linalg.norm(new_pt) # update counters step_ctr += 1 diff --git a/src/cytools/utils.py b/src/cytools/utils.py index a5c0110..dd03d41 100644 --- a/src/cytools/utils.py +++ b/src/cytools/utils.py @@ -37,6 +37,7 @@ # CYTools imports from cytools import config + # custom decorators # ----------------- # class instance caching @@ -47,7 +48,7 @@ def decorator(func): @functools.wraps(func) # copy func's metadata def wrapper(self, *args, **kwargs): # make class cache if it doesn't exist - if not hasattr(self, '_cache'): + if not hasattr(self, "_cache"): self._cache = {} # store function cache in class cache @@ -57,9 +58,12 @@ def wrapper(self, *args, **kwargs): # use cached result return self._cache[fname](self, *args, **kwargs) + return wrapper + return decorator + # basic math # ---------- def gcd_float(a: float, b: float, tol: float = 1e-5) -> float: @@ -92,11 +96,15 @@ def gcd_float(a: float, b: float, tol: float = 1e-5) -> float: # 0.09999999999999998 ``` """ - if abs(b) < tol: return abs(a) - return gcd_float(b, a%b, tol) + if abs(b) < tol: + return abs(a) + return gcd_float(b, a % b, tol) + + # variant that computes gcd over all elements in arr gcd_list = lambda arr: functools.reduce(gcd_float, arr) + # flint conversion # ---------------- def float_to_fmpq(c: float) -> flint.fmpq: @@ -123,6 +131,7 @@ def float_to_fmpq(c: float) -> flint.fmpq: f = fractions.Fraction(c).limit_denominator() return flint.fmpq(f.numerator, f.denominator) + def fmpq_to_float(c: flint.fmpq) -> float: """ **Description:** @@ -146,7 +155,8 @@ def fmpq_to_float(c: flint.fmpq) -> float: # (0.5, 0.3333333333333333, 2.45) ``` """ - return int(c.p)/int(c.q) + return int(c.p) / int(c.q) + def array_to_flint(arr: np.ndarray, t: "int | float" = None) -> np.ndarray: """ @@ -177,16 +187,22 @@ def array_to_flint(arr: np.ndarray, t: "int | float" = None) -> np.ndarray: ``` """ # type conversion function - if t is None: t = arr.dtype + if t is None: + t = arr.dtype - if t == int: f = lambda n: flint.fmpz(int(n)) - else: f = float_to_fmpq + if t == int: + f = lambda n: flint.fmpz(int(n)) + else: + f = float_to_fmpq return np.vectorize(f)(arr).astype(object) + + # some type-specific aliases -array_int_to_fmpz = lambda arr: array_to_flint(arr, t=int) +array_int_to_fmpz = lambda arr: array_to_flint(arr, t=int) array_float_to_fmpq = lambda arr: array_to_flint(arr, t=float) + def array_from_flint(arr: np.ndarray, t=None) -> np.ndarray: """ **Description:** @@ -209,16 +225,21 @@ def array_from_flint(arr: np.ndarray, t=None) -> np.ndarray: elif t == flint.fmpq: return np.vectorize(fmpq_to_float)(arr).astype(float) else: - raise ValueError(f"Input array had element of type {t}!" +\ - "This is not a flint type!") + raise ValueError( + f"Input array had element of type {t}!" + "This is not a flint type!" + ) + + # some type-specific aliases -array_fmpz_to_int = lambda arr: array_from_flint(arr,t=flint.fmpz) -array_fmpq_to_float = lambda arr: array_from_flint(arr,t=flint.fmpq) +array_fmpz_to_int = lambda arr: array_from_flint(arr, t=flint.fmpz) +array_fmpq_to_float = lambda arr: array_from_flint(arr, t=flint.fmpq) + # sparse conversions # ------------------ -def to_sparse(arr: "dict | list", - sparse_type: str = "dok") -> "sp.dok_matrix | sp.csr_matrix": +def to_sparse( + arr: "dict | list", sparse_type: str = "dok" +) -> "sp.dok_matrix | sp.csr_matrix": """ **Description:** Converts a (manually implemented) sparse matrix of the form @@ -249,28 +270,30 @@ def to_sparse(arr: "dict | list", """ # input checking if sparse_type not in ("dok", "csr"): - raise ValueError("sparse_type must be either \"dok\" or \"csr\".") + raise ValueError('sparse_type must be either "dok" or "csr".') # map all inputs to list case if isinstance(arr, dict): - arr = [list(ind)+[val] for ind, val in arr.items()] + arr = [list(ind) + [val] for ind, val in arr.items()] # map to numpy array arr = np.asarray(arr) # form emptry sparse matrix with appropriate dimensions - sp_mat = sp.dok_matrix( tuple(1+arr.max(axis=0)[:2]) ) + sp_mat = sp.dok_matrix(tuple(1 + arr.max(axis=0)[:2])) # fill in matrix for r in arr: - sp_mat[r[0],r[1]] = r[2] + sp_mat[r[0], r[1]] = r[2] # return in appropriate format - if sparse_type=="dok": return sp_mat - else: return sp.csr_matrix(sp_mat) + if sparse_type == "dok": + return sp_mat + else: + return sp.csr_matrix(sp_mat) -def symmetric_sparse_to_dense(tensor: dict, - basis: ArrayLike = None) -> np.ndarray: + +def symmetric_sparse_to_dense(tensor: dict, basis: ArrayLike = None) -> np.ndarray: """ **Description:** Converts a symmetric sparse tensor of the form {(a,b,...,c): M_ab...c, ...} @@ -303,11 +326,11 @@ def symmetric_sparse_to_dense(tensor: dict, if basis is not None: dim = np.asarray(basis).shape[1] else: - dim = 1 + max( set.union(*[set(inds) for inds in tensor.keys()]) ) + dim = 1 + max(set.union(*[set(inds) for inds in tensor.keys()])) - rank = len( next(iter(tensor.keys())) ) - t = type( next(iter(tensor.values())) ) - out = np.zeros((dim,)*rank, dtype=t) + rank = len(next(iter(tensor.keys()))) + t = type(next(iter(tensor.values()))) + out = np.zeros((dim,) * rank, dtype=t) # fill dense tensor for inds, val in tensor.items(): @@ -317,12 +340,12 @@ def symmetric_sparse_to_dense(tensor: dict, # apply basis transformation if basis is not None: for i in reversed(range(rank)): - out = np.tensordot(out, basis, axes=[[i],[1]]) + out = np.tensordot(out, basis, axes=[[i], [1]]) return out -def symmetric_dense_to_sparse(tensor: ArrayLike, - basis: ArrayLike = None) -> dict: + +def symmetric_dense_to_sparse(tensor: ArrayLike, basis: ArrayLike = None) -> dict: """ **Description:** Converts a dense symmetric tensor to a sparse tensor of the form @@ -357,7 +380,7 @@ def symmetric_dense_to_sparse(tensor: ArrayLike, tensor = np.array(tensor) rank = len(tensor.shape) - dim = set(tensor.shape) + dim = set(tensor.shape) if len(dim) != 1: raise ValueError("All dimensions must have the same length") dim = next(iter(dim)) @@ -365,19 +388,19 @@ def symmetric_dense_to_sparse(tensor: ArrayLike, # apply basis transformation if basis is not None: for i in reversed(range(rank)): - tensor = np.tensordot(tensor, basis, axes=[[i],[1]]) + tensor = np.tensordot(tensor, basis, axes=[[i], [1]]) # iterate over increasing indices, filling sparse tensor for ind in itertools.combinations_with_replacement(range(dim), rank): if tensor[ind] != 0: out[ind] = tensor[ind] - + return out + # other tensor operations # ----------------------- -def filter_tensor_indices(tensor: dict, - indices: list[int]) -> dict: +def filter_tensor_indices(tensor: dict, indices: list[int]) -> dict: """ **Description:** Selects a specific subset of indices from a tensor. @@ -412,24 +435,29 @@ def filter_tensor_indices(tensor: dict, ``` """ # map from index to its count in indices object - reindex = {ind:i for i,ind in enumerate(indices)} + reindex = {ind: i for i, ind in enumerate(indices)} # only keep entries whose indices match those in indices - filtered = {key:val for key,val in tensor.items() if\ - all(c in indices for c in key)} + filtered = { + key: val for key, val in tensor.items() if all(c in indices for c in key) + } # return reindexed tensor (order defined by indices input) - return {tuple(sorted(reindex[c] for c in key)):val for key,val in\ - filtered.items()} + return { + tuple(sorted(reindex[c] for c in key)): val for key, val in filtered.items() + } + # solve systems # ------------- -def solve_linear_system(M: sp.csr_matrix, - C: list[float], - backend: str = "all", - check: bool = True, - backend_error_tol: float = 1e-4, - verbosity: int = 0) -> np.ndarray: +def solve_linear_system( + M: sp.csr_matrix, + C: list[float], + backend: str = "all", + check: bool = True, + backend_error_tol: float = 1e-4, + verbosity: int = 0, +) -> np.ndarray: """ **Description:** Solves the sparse linear system M*x + C = 0. @@ -469,25 +497,30 @@ def solve_linear_system(M: sp.csr_matrix, if backend == "all": for s in backends[1:]: - solution = solve_linear_system(M, C, backend=s, check=check, - backend_error_tol=backend_error_tol, - verbosity=verbosity) + solution = solve_linear_system( + M, + C, + backend=s, + check=check, + backend_error_tol=backend_error_tol, + verbosity=verbosity, + ) if solution is not None: return solution elif backend == "sksparse": try: from sksparse.cholmod import cholesky_AAt + factor = cholesky_AAt(M.transpose()) - solution = factor(-M.transpose()*C) + solution = factor(-M.transpose() * C) except: if verbosity >= 1: print("Linear backend error: sksparse failed.") elif backend == "scipy": try: - solution = sp.linalg.spsolve(M.transpose()*M,\ - -M.transpose()*C).tolist() + solution = sp.linalg.spsolve(M.transpose() * M, -M.transpose() * C).tolist() except: if verbosity >= 1: print("Linear backend error: scipy failed.") @@ -507,11 +540,14 @@ def solve_linear_system(M: sp.csr_matrix, return solution + # set algebraic geometric bases # ----------------------------- -def set_divisor_basis(tv_or_cy: "ToricVariety | CalabiYau", - basis: ArrayLike, - include_origin: bool = True): +def set_divisor_basis( + tv_or_cy: "ToricVariety | CalabiYau", + basis: ArrayLike, + include_origin: bool = True, +): """ **Description:** Specifies a basis of divisors for the toric variety or Calabi-Yau manifold, @@ -573,7 +609,7 @@ def set_divisor_basis(tv_or_cy: "ToricVariety | CalabiYau", glsm_rnk = np.linalg.matrix_rank(glsm_cm) # grab basis information - b = np.array(basis, dtype=int) # (only integer bases are supported) + b = np.array(basis, dtype=int) # (only integer bases are supported) if len(b.shape) == 1: # input is a vector @@ -583,17 +619,16 @@ def set_divisor_basis(tv_or_cy: "ToricVariety | CalabiYau", if (min(b) < 0) or (max(b) >= glsm_cm.shape[1]): raise ValueError("Indices are not in appropriate range.") - if (glsm_rnk != np.linalg.matrix_rank(glsm_cm[:,b])) or\ - (glsm_rnk != len(b)): + if (glsm_rnk != np.linalg.matrix_rank(glsm_cm[:, b])) or (glsm_rnk != len(b)): raise ValueError("Input divisors do not form a basis.") - if abs(int(round(np.linalg.det(glsm_cm[:,b])))) != 1: + if abs(int(round(np.linalg.det(glsm_cm[:, b])))) != 1: raise ValueError("Only integer bases are supported.") # Save divisor basis self._divisor_basis = b self._divisor_basis_mat = np.zeros(glsm_cm.shape, dtype=int) - self._divisor_basis_mat[:,b] = np.eye(glsm_rnk, dtype=int) + self._divisor_basis_mat[:, b] = np.eye(glsm_rnk, dtype=int) # Construct dual basis of curves self._curve_basis = b @@ -602,31 +637,43 @@ def set_divisor_basis(tv_or_cy: "ToricVariety | CalabiYau", linrels = self.glsm_linear_relations() linrels_tmp = np.empty(linrels.shape, dtype=int) - linrels_tmp[:,:len(nobasis)] = linrels[:,nobasis] - linrels_tmp[:,len(nobasis):] = linrels[:,b] + linrels_tmp[:, : len(nobasis)] = linrels[:, nobasis] + linrels_tmp[:, len(nobasis) :] = linrels[:, b] linrels_tmp = flint.fmpz_mat(linrels_tmp.tolist()).hnf() linrels_tmp = np.array(linrels_tmp.tolist(), dtype=int) - + linrels_new = np.empty(linrels.shape, dtype=int) - linrels_new[:,nobasis] = linrels_tmp[:,:len(nobasis)] - linrels_new[:,b] = linrels_tmp[:,len(nobasis):] + linrels_new[:, nobasis] = linrels_tmp[:, : len(nobasis)] + linrels_new[:, b] = linrels_tmp[:, len(nobasis) :] self._curve_basis_mat = np.zeros(glsm_cm.shape, dtype=int) - self._curve_basis_mat[:,b] = np.eye(len(b),dtype=int) - sublat_ind = int(round(np.linalg.det(np.array(flint.fmpz_mat(linrels.tolist()).snf().tolist(), dtype=int)[:,:linrels.shape[0]]))) + self._curve_basis_mat[:, b] = np.eye(len(b), dtype=int) + sublat_ind = int( + round( + np.linalg.det( + np.array( + flint.fmpz_mat(linrels.tolist()).snf().tolist(), + dtype=int, + )[:, : linrels.shape[0]] + ) + ) + ) for nb in nobasis[::-1]: - tup = [(k,kk) for k,kk in enumerate(linrels_new[:,nb]) if kk] + tup = [(k, kk) for k, kk in enumerate(linrels_new[:, nb]) if kk] if sublat_ind % tup[-1][1] != 0: raise RuntimeError("Problem with linear relations") - i,ii = tup[-1] - self._curve_basis_mat[:,nb] = -self._curve_basis_mat.dot(linrels_new[i])//ii + i, ii = tup[-1] + self._curve_basis_mat[:, nb] = ( + -self._curve_basis_mat.dot(linrels_new[i]) // ii + ) elif len(b.shape) == 2: # input is a matrix if not config._exp_features_enabled: - raise Exception("The experimental features must be enabled to " - "use generic bases.") + raise Exception( + "The experimental features must be enabled to " "use generic bases." + ) # We start by checking if the input matrix looks right if np.linalg.matrix_rank(b) != glsm_rnk: @@ -634,55 +681,88 @@ def set_divisor_basis(tv_or_cy: "ToricVariety | CalabiYau", if b.shape == (glsm_rnk, glsm_cm.shape[1]): new_b = b - elif b.shape == (glsm_rnk, glsm_cm.shape[1]-1): + elif b.shape == (glsm_rnk, glsm_cm.shape[1] - 1): new_b = np.empty(glsm_cm.shape, dtype=int) - new_b[:,1:] = b - new_b[:,0] = 0 + new_b[:, 1:] = b + new_b[:, 0] = 0 else: raise ValueError("Input matrix has incorrect shape.") new_glsm_cm = new_b.dot(glsm_cm.T).T if np.linalg.matrix_rank(new_glsm_cm) != glsm_rnk: raise ValueError("Input divisors do not form a basis.") - if abs(int(round(np.linalg.det(np.array(flint.fmpz_mat(new_glsm_cm.tolist()).snf().tolist(),dtype=int)[:glsm_rnk,:glsm_rnk])))) != 1: + if ( + abs( + int( + round( + np.linalg.det( + np.array( + flint.fmpz_mat(new_glsm_cm.tolist()).snf().tolist(), + dtype=int, + )[:glsm_rnk, :glsm_rnk] + ) + ) + ) + ) + != 1 + ): raise ValueError("Input divisors do not form an integral basis.") self._divisor_basis = np.array(new_b) # Now we store a more convenient form of the matrix where we use the # linear relations to express them in terms of the default prime toric # divisors standard_basis = self.polytope().glsm_basis( - integral=True, - include_origin=True, - points=self.prime_toric_divisors()) + integral=True, + include_origin=True, + points=self.prime_toric_divisors(), + ) linrels = self.polytope().glsm_linear_relations( - include_origin=True, - points=self.prime_toric_divisors()) + include_origin=True, points=self.prime_toric_divisors() + ) self._divisor_basis_mat = np.array(new_b) - nobasis = np.array([i for i in range(glsm_cm.shape[1]) if i not in standard_basis]) - sublat_ind = int(round(np.linalg.det(np.array(flint.fmpz_mat(linrels.tolist()).snf().tolist(), dtype=int)[:,:linrels.shape[0]]))) + nobasis = np.array( + [i for i in range(glsm_cm.shape[1]) if i not in standard_basis] + ) + sublat_ind = int( + round( + np.linalg.det( + np.array( + flint.fmpz_mat(linrels.tolist()).snf().tolist(), + dtype=int, + )[:, : linrels.shape[0]] + ) + ) + ) for nb in nobasis[::-1]: - tup = [(k,kk) for k,kk in enumerate(linrels[:,nb]) if kk] + tup = [(k, kk) for k, kk in enumerate(linrels[:, nb]) if kk] if sublat_ind % tup[-1][1] != 0: raise RuntimeError("Problem with linear relations") - i,ii = tup[-1] + i, ii = tup[-1] for j in range(self._divisor_basis_mat.shape[0]): - self._divisor_basis_mat[j] -= self._divisor_basis_mat[j,nb]*linrels[i] + self._divisor_basis_mat[j] -= ( + self._divisor_basis_mat[j, nb] * linrels[i] + ) # Finally, we invert the matrix and construct the dual curve basis - if abs(int(round(np.linalg.det(self._divisor_basis_mat[:,standard_basis])))) != 1: + if ( + abs(int(round(np.linalg.det(self._divisor_basis_mat[:, standard_basis])))) + != 1 + ): raise ValueError("Input divisors do not form an integral basis.") - inv_mat = flint.fmpz_mat(self._divisor_basis_mat[:,standard_basis].tolist()).inv(integer=True) + inv_mat = flint.fmpz_mat( + self._divisor_basis_mat[:, standard_basis].tolist() + ).inv(integer=True) inv_mat = np.array(inv_mat.tolist(), dtype=int) # flint sometimes returns the negative inverse - if inv_mat.dot(self._divisor_basis_mat[:,standard_basis])[0,0] == -1: - inv_mat *= -1; + if inv_mat.dot(self._divisor_basis_mat[:, standard_basis])[0, 0] == -1: + inv_mat *= -1 self._curve_basis_mat = np.zeros(glsm_cm.shape, dtype=int) - self._curve_basis_mat[:,standard_basis] = np.array(inv_mat).T + self._curve_basis_mat[:, standard_basis] = np.array(inv_mat).T for nb in nobasis[::-1]: - tup = [(k,kk) for k,kk in enumerate(linrels[:,nb]) if kk] + tup = [(k, kk) for k, kk in enumerate(linrels[:, nb]) if kk] if sublat_ind % tup[-1][1] != 0: raise RuntimeError("Problem with linear relations") - i,ii = tup[-1] - self._curve_basis_mat[:,nb] = -self._curve_basis_mat.dot(linrels[i])//ii + i, ii = tup[-1] + self._curve_basis_mat[:, nb] = -self._curve_basis_mat.dot(linrels[i]) // ii self._curve_basis = np.array(self._curve_basis_mat) else: raise ValueError("Input must be either a vector or a matrix.") @@ -690,9 +770,11 @@ def set_divisor_basis(tv_or_cy: "ToricVariety | CalabiYau", self.clear_cache(recursive=False, only_in_basis=True) -def set_curve_basis(tv_or_cy: "ToricVariety | CalabiYau", - basis: ArrayLike, - include_origin: bool = True): +def set_curve_basis( + tv_or_cy: "ToricVariety | CalabiYau", + basis: ArrayLike, + include_origin: bool = True, +): """ **Description:** Specifies a basis of curves of the toric variety, which in turn specifies a @@ -752,7 +834,7 @@ def set_curve_basis(tv_or_cy: "ToricVariety | CalabiYau", advanced example involving generic bases these two functions differ. An example can be found in the [experimental features](./experimental) section. """ - self = tv_or_cy # More conveninent to work with + self = tv_or_cy # More conveninent to work with # parse basis b = np.array(basis, dtype=int) @@ -764,11 +846,12 @@ def set_curve_basis(tv_or_cy: "ToricVariety | CalabiYau", if len(b.shape) != 2: raise ValueError("Input must be either a vector or a matrix.") - + # Else input is a matrix if not config._exp_features_enabled: - raise Exception("The experimental features must be enabled to " - "use generic bases.") + raise Exception( + "The experimental features must be enabled to " "use generic bases." + ) # grab GLSM information glsm_cm = self.glsm_charge_matrix(include_origin=True) @@ -778,32 +861,48 @@ def set_curve_basis(tv_or_cy: "ToricVariety | CalabiYau", raise ValueError("Input matrix has incorrect rank.") if b.shape == (glsm_rnk, glsm_cm.shape[1]): new_b = b - elif b.shape == (glsm_rnk, glsm_cm.shape[1]-1): + elif b.shape == (glsm_rnk, glsm_cm.shape[1] - 1): new_b = np.empty(glsm_cm.shape, dtype=t) - new_b[:,1:] = b - new_b[:,0] = -np.sum(b, axis=1) + new_b[:, 1:] = b + new_b[:, 0] = -np.sum(b, axis=1) else: raise ValueError("Input matrix has incorrect shape.") - pts = [tuple(pt)+(1,) for pt in self.polytope().points()[[0]+list(self.prime_toric_divisors())]] + pts = [ + tuple(pt) + (1,) + for pt in self.polytope().points()[[0] + list(self.prime_toric_divisors())] + ] if any(new_b.dot(pts).flat): raise ValueError("Input curves do not form a valid basis.") - if abs(int(round(np.linalg.det(np.array(flint.fmpz_mat(new_b.tolist()).snf().tolist(),dtype=int)[:glsm_rnk,:glsm_rnk])))) != 1: + if ( + abs( + int( + round( + np.linalg.det( + np.array( + flint.fmpz_mat(new_b.tolist()).snf().tolist(), + dtype=int, + )[:glsm_rnk, :glsm_rnk] + ) + ) + ) + ) + != 1 + ): raise ValueError("Input divisors do not form an integral basis.") standard_basis = self.polytope().glsm_basis( - integral=True, - include_origin=True, - points=self.prime_toric_divisors()) - if abs(int(round(np.linalg.det(new_b[:,standard_basis])))) != 1: + integral=True, include_origin=True, points=self.prime_toric_divisors() + ) + if abs(int(round(np.linalg.det(new_b[:, standard_basis])))) != 1: raise ValueError("Input divisors do not form an integral basis.") - inv_mat = flint.fmpz_mat(new_b[:,standard_basis].tolist()).inv(integer=True) + inv_mat = flint.fmpz_mat(new_b[:, standard_basis].tolist()).inv(integer=True) inv_mat = np.array(inv_mat.tolist(), dtype=int) # flint sometimes returns the negative inverse - if inv_mat.dot(new_b[:,standard_basis])[0,0] == -1: - inv_mat *= -1; + if inv_mat.dot(new_b[:, standard_basis])[0, 0] == -1: + inv_mat *= -1 self._divisor_basis_mat = np.zeros(glsm_cm.shape, dtype=int) - self._divisor_basis_mat[:,standard_basis] = np.array(inv_mat).T + self._divisor_basis_mat[:, standard_basis] = np.array(inv_mat).T self._divisor_basis = np.array(self._divisor_basis_mat) self._curve_basis = np.array(new_b) self._curve_basis_mat = np.array(new_b) @@ -811,16 +910,19 @@ def set_curve_basis(tv_or_cy: "ToricVariety | CalabiYau", # Clear the cache of all in-basis computations self.clear_cache(recursive=False, only_in_basis=True) + # polytope grabbing # ----------------- -def polytope_generator(input: str, - input_type: str = "file", - format: str = "ks", - backend: str = None, - dualize: bool = False, - favorable: bool = None, - lattice: str = None, - limit: int = None) -> Generator["Polytope", None, None]: +def polytope_generator( + input: str, + input_type: str = "file", + format: str = "ks", + backend: str = None, + dualize: bool = False, + favorable: bool = None, + lattice: str = None, + limit: int = None, +) -> Generator["Polytope", None, None]: """ **Description:** Reads polytopes from a file or a string. The polytopes can be specified @@ -889,17 +991,19 @@ def polytope_generator(input: str, else: in_string = input.split("\n") l = in_string.pop(0) - + if format == "ws": # read the polytopes as weight systems while (limit is None) or (n_yielded < limit): # pass line to PALP - palp = subprocess.Popen((config.palp_path + "/poly.x", "-v"), - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True) - palp_res, palp_err = palp.communicate(input=l+"\n") + palp = subprocess.Popen( + (config.palp_path + "/poly.x", "-v"), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + palp_res, palp_err = palp.communicate(input=l + "\n") palp_res = palp_res.split("\n") # add vertices @@ -914,14 +1018,15 @@ def polytope_generator(input: str, vert = np.array(vert) # ensure reasonable shape - if len(vert.shape) == 0: break - if vert.shape[0] < vert.shape[1]: vert = vert.T + if len(vert.shape) == 0: + break + if vert.shape[0] < vert.shape[1]: + vert = vert.T # build the Polytope p = Polytope(vert, backend=backend) - if (favorable is None) or\ - (p.is_favorable(lattice=lattice) == favorable): + if (favorable is None) or (p.is_favorable(lattice=lattice) == favorable): n_yielded += 1 yield (p.dual() if dualize else p) @@ -930,10 +1035,12 @@ def polytope_generator(input: str, l = in_file.readline() for i in range(5): - if l != "": break + if l != "": + break l = in_file.readline() else: - in_file.close(); break + in_file.close() + break else: if len(in_string) > 0: l = in_string.pop(0) @@ -966,8 +1073,7 @@ def polytope_generator(input: str, # build the Polytope p = Polytope(vert, backend=backend) - if (favorable is None) or\ - (p.is_favorable(lattice=lattice) == favorable): + if (favorable is None) or (p.is_favorable(lattice=lattice) == favorable): n_yielded += 1 yield (p.dual() if dualize else p) @@ -976,25 +1082,30 @@ def polytope_generator(input: str, l = in_file.readline() for i in range(5): - if l != "": break + if l != "": + break l = in_file.readline() else: - in_file.close(); break + in_file.close() + break else: if len(in_string) > 0: l = in_string.pop(0) else: break -def read_polytopes(input: str, - input_type: str = "file", - format: str = "ks", - backend: str = None, - as_list: bool = False, - dualize: bool = False, - favorable: bool = None, - lattice: str = None, - limit: int = None) -> 'Generator["Polytope", None, None] | \ + +def read_polytopes( + input: str, + input_type: str = "file", + format: str = "ks", + backend: str = None, + as_list: bool = False, + dualize: bool = False, + favorable: bool = None, + lattice: str = None, + limit: int = None, +) -> 'Generator["Polytope", None, None] | \ list["Polytope"]': """ **Description:** @@ -1041,30 +1152,44 @@ def read_polytopes(input: str, # [A 4-dimensional reflexive lattice polytope in ZZ^4] ``` """ - g = polytope_generator(input, input_type=input_type, format=format, - backend=backend, dualize=dualize, - favorable=favorable, lattice=lattice, limit=limit) - - if as_list: return list(g) - else: return g - -def fetch_polytopes(h11: int = None, h12: int = None, - h13: int = None, h21: int = None, - h22: int = None, h31: int = None, - chi: int = None, - lattice: str = None, - dim: int = 4, - n_points: int = None, - n_vertices: int = None, - n_dual_points: int = None, - n_facets: int = None, - limit: int = 1000, - timeout: int = 60, - as_list: bool = False, - backend: str = None, - dualize: bool = False, - favorable: bool = None) ->\ - 'Generator["Polytope", None, None] | list["Polytope"]': + g = polytope_generator( + input, + input_type=input_type, + format=format, + backend=backend, + dualize=dualize, + favorable=favorable, + lattice=lattice, + limit=limit, + ) + + if as_list: + return list(g) + else: + return g + + +def fetch_polytopes( + h11: int = None, + h12: int = None, + h13: int = None, + h21: int = None, + h22: int = None, + h31: int = None, + chi: int = None, + lattice: str = None, + dim: int = 4, + n_points: int = None, + n_vertices: int = None, + n_dual_points: int = None, + n_facets: int = None, + limit: int = 1000, + timeout: int = 60, + as_list: bool = False, + backend: str = None, + dualize: bool = False, + favorable: bool = None, +) -> 'Generator["Polytope", None, None] | list["Polytope"]': """ **Description:** Fetches reflexive polytopes from the Kreuzer-Skarke database or from the @@ -1135,7 +1260,7 @@ def fetch_polytopes(h11: int = None, h12: int = None, """ # input checking # -------------- - if dim not in (4,5): + if dim not in (4, 5): raise ValueError("Only polytopes of dimension 4 or 5 are available.") if lattice not in ("N", "M", None): @@ -1143,10 +1268,9 @@ def fetch_polytopes(h11: int = None, h12: int = None, if favorable is not None: if lattice is None: - raise ValueError("Must specify lattice when checking " - "favorability.") + raise ValueError("Must specify lattice when checking " "favorability.") - fetch_limit = (5 if favorable else 10)*limit + 100 + fetch_limit = (5 if favorable else 10) * limit + 100 else: fetch_limit = limit @@ -1157,8 +1281,10 @@ def fetch_polytopes(h11: int = None, h12: int = None, if (h13 is not None) and (h31 is not None) and (h13 != h31): raise ValueError("Only one of h13 or h31 should be specified.") - if (h12 is None) and (h21 is not None): h12 = h21 - if (h13 is None) and (h31 is not None): h13 = h31 + if (h12 is None) and (h21 is not None): + h12 = h21 + if (h13 is None) and (h31 is not None): + h13 = h31 # grab the polytopes # ------------------ @@ -1167,42 +1293,71 @@ def fetch_polytopes(h11: int = None, h12: int = None, if h13 is not None or h22 is not None: print("Ignoring inputs for h13 and h22.") - if (lattice is None) and \ - ((h11 is not None) or (h12 is not None) or (chi is not None)): - raise ValueError("Lattice must be specified when Hodge numbers " - "or Euler characteristic are given.") + if (lattice is None) and ( + (h11 is not None) or (h12 is not None) or (chi is not None) + ): + raise ValueError( + "Lattice must be specified when Hodge numbers " + "or Euler characteristic are given." + ) if lattice == "N": h11, h12 = h12, h11 - chi = (-chi if chi is not None else None) - - if (chi is not None) and (h11 is not None) and (h12 is not None) and\ - (chi != 2*(h11-h21)): + chi = -chi if chi is not None else None + + if ( + (chi is not None) + and (h11 is not None) + and (h12 is not None) + and (chi != 2 * (h11 - h21)) + ): raise ValueError("Inconsistent Euler characteristic input.") # build/send a request - variables = [h11, h12, n_points, n_vertices, n_dual_points, n_facets, - chi, fetch_limit] + variables = [ + h11, + h12, + n_points, + n_vertices, + n_dual_points, + n_facets, + chi, + fetch_limit, + ] names = ["h11", "h12", "M", "V", "N", "F", "chi", "L"] - parameters = {name:str(var) for name,var in zip(names, variables)\ - if var is not None} + parameters = { + name: str(var) for name, var in zip(names, variables) if var is not None + } - r = requests.get("http://quark.itp.tuwien.ac.at/cgi-bin/cy/cydata.cgi", - params=parameters, timeout=timeout) + r = requests.get( + "http://quark.itp.tuwien.ac.at/cgi-bin/cy/cydata.cgi", + params=parameters, + timeout=timeout, + ) else: # further input checking... if (lattice is None) and ((h11 is not None) or (h13 is not None)): - raise ValueError("Lattice must be specified when h11 or h13 " - "are given.") - - if lattice == "N": h11, h13 = h13, h11 + raise ValueError("Lattice must be specified when h11 or h13 " "are given.") - if (chi is not None) and (h11 is not None) and (h12 is not None) and\ - (h13 is not None) and (chi != 48+6*(h11-h12+h13)): + if lattice == "N": + h11, h13 = h13, h11 + + if ( + (chi is not None) + and (h11 is not None) + and (h12 is not None) + and (h13 is not None) + and (chi != 48 + 6 * (h11 - h12 + h13)) + ): raise ValueError("Inconsistent Euler characteristic input.") - if (h22 is not None) and (h11 is not None) and (h12 is not None) and\ - (h13 is not None) and (h22 != 44+6*h11-2*h12+4*h13): + if ( + (h22 is not None) + and (h11 is not None) + and (h12 is not None) + and (h13 is not None) + and (h22 != 44 + 6 * h11 - 2 * h12 + 4 * h13) + ): raise ValueError("Inconsistent h22 input.") # build/send a request @@ -1210,21 +1365,30 @@ def fetch_polytopes(h11: int = None, h12: int = None, names = ["h11", "h12", "h13", "h22", "chi", "limit"] url = "http://rgc.itp.tuwien.ac.at/fourfolds/db/5d_reflexive" - for i,vr in enumerate(variables): - if vr is not None: url += f",{names[i]}={vr}" + for i, vr in enumerate(variables): + if vr is not None: + url += f",{names[i]}={vr}" url += ".txt" r = requests.get(url, timeout=timeout) # return the generator based off of output of request - return read_polytopes(r.text, input_type="str", - format=("ks" if dim==4 else "ws"), - backend=backend, as_list=as_list, dualize=dualize, - favorable=favorable, lattice=lattice, limit=limit) + return read_polytopes( + r.text, + input_type="str", + format=("ks" if dim == 4 else "ws"), + backend=backend, + as_list=as_list, + dualize=dualize, + favorable=favorable, + lattice=lattice, + limit=limit, + ) + # point manipulations # ------------------- -def lll_reduce(pts_in: ArrayLike, transform: bool=False) -> "misc": +def lll_reduce(pts_in: ArrayLike, transform: bool = False) -> "misc": """ Apply lll-reduction to the input points (the rows). @@ -1237,22 +1401,22 @@ def lll_reduce(pts_in: ArrayLike, transform: bool=False) -> "misc": (A, Ainv) s.t. pts_red.T = A*pts_in.T. As numpy arrays. """ pts = np.array(pts_in) - + # lll-reduction - pts = pts.T # map points to columns for lll-algorithm + pts = pts.T # map points to columns for lll-algorithm - if transform==True: + if transform == True: pts_red, transf = flint.fmpz_mat(pts.tolist()).lll(transform=True) else: pts_red = flint.fmpz_mat(pts.tolist()).lll(transform=False) - pts_red = pts_red.transpose() # map points back to rows + pts_red = pts_red.transpose() # map points back to rows # convert to numpy pts_red = np.array(pts_red.tolist(), dtype=int) - if transform==True: - A = np.array(transf.tolist(), dtype=int) + if transform == True: + A = np.array(transf.tolist(), dtype=int) Ainv = np.array(transf.inv(integer=True).tolist(), dtype=int) # check that Ainv is indeed an inverse @@ -1271,11 +1435,12 @@ def lll_reduce(pts_in: ArrayLike, transform: bool=False) -> "misc": else: return pts_red + def find_new_affinely_independent_points(pts: ArrayLike) -> np.ndarray: """ **Description:** Finds new points that are affinely independent to the input list of points. - + This is useful when one wants to turn a polytope that is not full-dimensional into one that is, without affecting the structure of the triangulations. @@ -1307,30 +1472,31 @@ def find_new_affinely_independent_points(pts: ArrayLike) -> np.ndarray: translation = pts[0].copy() pts -= translation - if shape[0]==1: - pts = np.append(pts_trans, [[1]+[0]*(shape[1]-1)], axis=0) - + if shape[0] == 1: + pts = np.append(pts_trans, [[1] + [0] * (shape[1] - 1)], axis=0) + dim = np.linalg.matrix_rank(pts) - + # make basis of points basis = [] basis_dim = 0 for pt in pts: basis.append(pt.tolist()) - - new_rank = np.linalg.matrix_rank(basis) + + new_rank = np.linalg.matrix_rank(basis) if basis_dim < new_rank: basis_dim = new_rank else: basis.pop() - if basis_dim == dim: break + if basis_dim == dim: + break # find independent points k, n_k = flint.fmpz_mat(basis).nullspace() - new_pts = np.array(k.transpose().tolist(), dtype=int)[:n_k,:] + new_pts = np.array(k.transpose().tolist(), dtype=int)[:n_k, :] if shape[0] == 1: - new_pts = np.append(new_pts, [[1]+[0]*(shape[1]-1)], axis=0) + new_pts = np.append(new_pts, [[1] + [0] * (shape[1] - 1)], axis=0) - return new_pts+translation + return new_pts + translation diff --git a/unittests/test_all_functions.py b/unittests/test_all_functions.py index 3bb55c2..6c79679 100644 --- a/unittests/test_all_functions.py +++ b/unittests/test_all_functions.py @@ -3,8 +3,8 @@ # Here we simply run most functions with most parameter combinations to see if anything crashes print("Testing polytope functions...") -p = Polytope([[0],[1]]) -p = Polytope([[1,1],[2,1]]) +p = Polytope([[0], [1]]) +p = Polytope([[1, 1], [2, 1]]) p = next(fetch_polytopes(dim=5, h11=100, lattice="N")) p = next(fetch_polytopes(h11=11, h21=491, lattice="N")) poly_pts = p.points() @@ -21,7 +21,7 @@ p.ambient_dim() p.dim() p.is_solid() - #p._pts_saturated() + # p._pts_saturated() p.points() p.interior_points() p.boundary_points() @@ -36,16 +36,16 @@ p.boundary_points_not_interior_to_facets(as_indices=True) p.points_not_interior_to_facets(as_indices=True) p.is_reflexive() - p.hpq(0,0,lattice="N") - p.hpq(0,1,lattice="N") - p.hpq(1,1,lattice="N") - p.hpq(1,2,lattice="N") - p.hpq(2,2,lattice="N") - p.hpq(0,0,lattice="M") - p.hpq(0,1,lattice="M") - p.hpq(1,1,lattice="M") - p.hpq(1,2,lattice="M") - p.hpq(2,2,lattice="M") + p.hpq(0, 0, lattice="N") + p.hpq(0, 1, lattice="N") + p.hpq(1, 1, lattice="N") + p.hpq(1, 2, lattice="N") + p.hpq(2, 2, lattice="N") + p.hpq(0, 0, lattice="M") + p.hpq(0, 1, lattice="M") + p.hpq(1, 1, lattice="M") + p.hpq(1, 2, lattice="M") + p.hpq(2, 2, lattice="M") p.h11(lattice="N") p.h21(lattice="N") p.h22(lattice="N") @@ -73,7 +73,9 @@ p.glsm_charge_matrix(include_origin=False, include_points_interior_to_facets=True) p.glsm_linear_relations() p.glsm_linear_relations(include_points_interior_to_facets=True) - p.glsm_linear_relations(include_origin=False, include_points_interior_to_facets=True) + p.glsm_linear_relations( + include_origin=False, include_points_interior_to_facets=True + ) p.glsm_basis() p.glsm_basis(include_points_interior_to_facets=True) p.glsm_basis(include_origin=False, include_points_interior_to_facets=True) @@ -101,9 +103,82 @@ p.triangulate(backend=b, make_star=False) p.triangulate(backend=b, include_points_interior_to_facets=True) p.triangulate(backend=b, make_star=False) - p.triangulate(backend=b, heights=[-9.2,2.7,2.7,4.5,4.6,5.4,2.2,-0.5,-2.2,-1.7,-2.8,-1.7,-2.5,-1.5,-1.2,1.2], verbosity=0) - p.triangulate(backend=b, heights=[10,2,2,4,4,5,2,0,-2,-1,-2,-1,-2,-1,-1,1], verbosity=0) - t = p.triangulate(backend=b, simplices=[[0,1,3,4,5],[0,1,3,4,15],[0,1,3,5,15],[0,1,4,5,13],[0,1,4,13,15],[0,1,5,11,13],[0,1,5,11,14],[0,1,5,14,15],[0,1,6,7,9],[0,1,6,7,11],[0,1,6,9,11],[0,1,7,8,9],[0,1,7,8,11],[0,1,8,9,10],[0,1,8,10,11],[0,1,9,10,13],[0,1,9,11,13],[0,1,10,11,12],[0,1,10,12,13],[0,1,11,12,14],[0,1,12,13,14],[0,1,13,14,15],[0,2,3,4,5],[0,2,3,4,15],[0,2,3,5,15],[0,2,4,5,13],[0,2,4,13,15],[0,2,5,11,13],[0,2,5,11,14],[0,2,5,14,15],[0,2,6,7,9],[0,2,6,7,11],[0,2,6,9,11],[0,2,7,8,9],[0,2,7,8,11],[0,2,8,9,10],[0,2,8,10,11],[0,2,9,10,13],[0,2,9,11,13],[0,2,10,11,12],[0,2,10,12,13],[0,2,11,12,14],[0,2,12,13,14],[0,2,13,14,15]]) + p.triangulate( + backend=b, + heights=[ + -9.2, + 2.7, + 2.7, + 4.5, + 4.6, + 5.4, + 2.2, + -0.5, + -2.2, + -1.7, + -2.8, + -1.7, + -2.5, + -1.5, + -1.2, + 1.2, + ], + verbosity=0, + ) + p.triangulate( + backend=b, + heights=[10, 2, 2, 4, 4, 5, 2, 0, -2, -1, -2, -1, -2, -1, -1, 1], + verbosity=0, + ) + t = p.triangulate( + backend=b, + simplices=[ + [0, 1, 3, 4, 5], + [0, 1, 3, 4, 15], + [0, 1, 3, 5, 15], + [0, 1, 4, 5, 13], + [0, 1, 4, 13, 15], + [0, 1, 5, 11, 13], + [0, 1, 5, 11, 14], + [0, 1, 5, 14, 15], + [0, 1, 6, 7, 9], + [0, 1, 6, 7, 11], + [0, 1, 6, 9, 11], + [0, 1, 7, 8, 9], + [0, 1, 7, 8, 11], + [0, 1, 8, 9, 10], + [0, 1, 8, 10, 11], + [0, 1, 9, 10, 13], + [0, 1, 9, 11, 13], + [0, 1, 10, 11, 12], + [0, 1, 10, 12, 13], + [0, 1, 11, 12, 14], + [0, 1, 12, 13, 14], + [0, 1, 13, 14, 15], + [0, 2, 3, 4, 5], + [0, 2, 3, 4, 15], + [0, 2, 3, 5, 15], + [0, 2, 4, 5, 13], + [0, 2, 4, 13, 15], + [0, 2, 5, 11, 13], + [0, 2, 5, 11, 14], + [0, 2, 5, 14, 15], + [0, 2, 6, 7, 9], + [0, 2, 6, 7, 11], + [0, 2, 6, 9, 11], + [0, 2, 7, 8, 9], + [0, 2, 7, 8, 11], + [0, 2, 8, 9, 10], + [0, 2, 8, 10, 11], + [0, 2, 9, 10, 13], + [0, 2, 9, 11, 13], + [0, 2, 10, 11, 12], + [0, 2, 10, 12, 13], + [0, 2, 11, 12, 14], + [0, 2, 12, 13, 14], + [0, 2, 13, 14, 15], + ], + ) t.__repr__() t == t t != t @@ -156,7 +231,7 @@ cy.polytope() cy.ambient_dim() cy.dim() -cy.hpq(1,1) +cy.hpq(1, 1) cy.h11() cy.h12() cy.h13() diff --git a/unittests/test_compare_cy_to_Sage.py b/unittests/test_compare_cy_to_Sage.py index aeb0940..41ec2c5 100644 --- a/unittests/test_compare_cy_to_Sage.py +++ b/unittests/test_compare_cy_to_Sage.py @@ -5,97 +5,145 @@ import cytools as cyt import numpy as np -test_data_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/cy_data.json') +test_data_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "data/cy_data.json" +) + class Intersection_Number_Sage_Comparison_Test(unittest.TestCase): def setUp(self): - with open(test_data_path,'r') as fp: + with open(test_data_path, "r") as fp: self.test_data = json.load(fp) def test_intersection_numbers(self): print("Testing: Intersection numbers...") try: from tqdm import tqdm + data_iter = tqdm(self.test_data) except: data_iter = self.test_data for doc in data_iter: - vertices = eval(doc['vertices']) - sage_points = [list(i) for i in eval(doc['rays'])] - sage_favorable = eval(doc['N_favorable']) + vertices = eval(doc["vertices"]) + sage_points = [list(i) for i in eval(doc["rays"])] + sage_favorable = eval(doc["N_favorable"]) if sage_favorable: poly = cyt.Polytope(vertices) cyt_points = poly.boundary_points_not_interior_to_facets() frst = poly.triangulate(make_star=True) cy = frst.get_cy() - cyt_int_nums_origin_raw = cy.intersection_numbers(in_basis=False, - zero_as_anticanonical=True) - cyt_int_nums_raw = [[ii[0]-1, ii[1]-1, ii[2]-1] + [cyt_int_nums_origin_raw[ii]] - for ii in cyt_int_nums_origin_raw if ii[0]!=0 and ii[1]!=0 and ii[2]!=0] - sage_int_nums_raw = eval(doc['intersection_numbers']) - sage_index = {tuple(pp):p for p,pp in enumerate(sage_points)} - cyt_index = {tuple(pp):p for p,pp in enumerate(cyt_points)} - transform_dict_1 = {sage_index[tuple(pp)]:cyt_index[tuple(pp)] for pp in cyt_points} - sage_int_nums = sorted([sorted( - [transform_dict_1[ii[0]],transform_dict_1[ii[1]],transform_dict_1[ii[2]]]) - + [ii[3]] for ii in sage_int_nums_raw]) + cyt_int_nums_origin_raw = cy.intersection_numbers( + in_basis=False, zero_as_anticanonical=True + ) + cyt_int_nums_raw = [ + [ii[0] - 1, ii[1] - 1, ii[2] - 1] + [cyt_int_nums_origin_raw[ii]] + for ii in cyt_int_nums_origin_raw + if ii[0] != 0 and ii[1] != 0 and ii[2] != 0 + ] + sage_int_nums_raw = eval(doc["intersection_numbers"]) + sage_index = {tuple(pp): p for p, pp in enumerate(sage_points)} + cyt_index = {tuple(pp): p for p, pp in enumerate(cyt_points)} + transform_dict_1 = { + sage_index[tuple(pp)]: cyt_index[tuple(pp)] for pp in cyt_points + } + sage_int_nums = sorted( + [ + sorted( + [ + transform_dict_1[ii[0]], + transform_dict_1[ii[1]], + transform_dict_1[ii[2]], + ] + ) + + [ii[3]] + for ii in sage_int_nums_raw + ] + ) cyt_int_nums = sorted([ii for ii in cyt_int_nums_raw]) - self.assertTrue(sage_int_nums==cyt_int_nums, - msg="Intersection numbers don't match. \n Sage: \n" - + str(sage_int_nums) + "\n Cytools: \n" + str(cyt_int_nums)) + self.assertTrue( + sage_int_nums == cyt_int_nums, + msg="Intersection numbers don't match. \n Sage: \n" + + str(sage_int_nums) + + "\n Cytools: \n" + + str(cyt_int_nums), + ) def test_sr_ideal(self): print("Testing: Stanley-Reisner ideal...") try: from tqdm import tqdm + data_iter = tqdm(self.test_data[5:]) except: data_iter = self.test_data[5:] for doc in data_iter: - vertices = eval(doc['vertices']) - sage_points = [list(i) for i in eval(doc['rays'])] - sage_favorable = eval(doc['N_favorable']) + vertices = eval(doc["vertices"]) + sage_points = [list(i) for i in eval(doc["rays"])] + sage_favorable = eval(doc["N_favorable"]) if sage_favorable: poly = cyt.Polytope(vertices) cyt_points = poly.boundary_points_not_interior_to_facets() frst = poly.triangulate(make_star=True) cy = frst.get_cy() cyt_sr_ideal_raw = frst.sr_ideal() - sage_sr_ideal_raw = eval(doc['sr_ideal']) - sage_index = {tuple(pp):p for p,pp in enumerate(sage_points)} - cyt_index = {tuple(pp):p for p,pp in enumerate(cyt_points)} - transform_dict_1 = {sage_index[tuple(pp)]:cyt_index[tuple(pp)] for pp in cyt_points} - sage_sr_ideal = sorted([sorted([transform_dict_1[ss]+1 for ss in s]) for s in sage_sr_ideal_raw]) + sage_sr_ideal_raw = eval(doc["sr_ideal"]) + sage_index = {tuple(pp): p for p, pp in enumerate(sage_points)} + cyt_index = {tuple(pp): p for p, pp in enumerate(cyt_points)} + transform_dict_1 = { + sage_index[tuple(pp)]: cyt_index[tuple(pp)] for pp in cyt_points + } + sage_sr_ideal = sorted( + [ + sorted([transform_dict_1[ss] + 1 for ss in s]) + for s in sage_sr_ideal_raw + ] + ) cyt_sr_ideal = sorted([sorted(s) for s in cyt_sr_ideal_raw]) - self.assertTrue(sage_sr_ideal==cyt_sr_ideal, - msg="Stanley-Reisner ideals don't match. \n Sage: \n" - + str(sage_sr_ideal) + "\n Cytools: \n" + str(cyt_sr_ideal)) + self.assertTrue( + sage_sr_ideal == cyt_sr_ideal, + msg="Stanley-Reisner ideals don't match. \n Sage: \n" + + str(sage_sr_ideal) + + "\n Cytools: \n" + + str(cyt_sr_ideal), + ) def test_second_chern_class(self): print("Testing: Second Chern class...") try: from tqdm import tqdm + data_iter = tqdm(self.test_data) except: data_iter = self.test_data for doc in data_iter: - vertices = eval(doc['vertices']) - sage_points = [list(i) for i in eval(doc['rays'])] - sage_favorable = eval(doc['N_favorable']) + vertices = eval(doc["vertices"]) + sage_points = [list(i) for i in eval(doc["rays"])] + sage_favorable = eval(doc["N_favorable"]) if sage_favorable: poly = cyt.Polytope(vertices) cyt_points = poly.boundary_points_not_interior_to_facets() frst = poly.triangulate(make_star=True) cy = frst.get_cy() - sage_c2_raw = eval(doc['c2']) - cyt_c2 = cy.second_chern_class(in_basis=False, include_origin=False).tolist() - sage_index = {tuple(pp):p for p,pp in enumerate(sage_points)} - cyt_index = {tuple(pp):p for p,pp in enumerate(cyt_points)} - transform_dict = {cyt_index[tuple(pp)]:sage_index[tuple(pp)] for pp in cyt_points} - sage_c2 = [sage_c2_raw[transform_dict[c]] for c in range(len(sage_c2_raw))] - self.assertTrue(sage_c2==cyt_c2, - msg="Second Chern class doesn't match. \n Sage: \n" - + str(sage_c2) + "\n Cytools: \n" + str(cyt_c2)) - -if __name__ == '__main__': - unittest.main() \ No newline at end of file + sage_c2_raw = eval(doc["c2"]) + cyt_c2 = cy.second_chern_class( + in_basis=False, include_origin=False + ).tolist() + sage_index = {tuple(pp): p for p, pp in enumerate(sage_points)} + cyt_index = {tuple(pp): p for p, pp in enumerate(cyt_points)} + transform_dict = { + cyt_index[tuple(pp)]: sage_index[tuple(pp)] for pp in cyt_points + } + sage_c2 = [ + sage_c2_raw[transform_dict[c]] for c in range(len(sage_c2_raw)) + ] + self.assertTrue( + sage_c2 == cyt_c2, + msg="Second Chern class doesn't match. \n Sage: \n" + + str(sage_c2) + + "\n Cytools: \n" + + str(cyt_c2), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/unittests/test_compare_poly_to_Sage.py b/unittests/test_compare_poly_to_Sage.py index 9af423c..50bf58d 100644 --- a/unittests/test_compare_poly_to_Sage.py +++ b/unittests/test_compare_poly_to_Sage.py @@ -5,100 +5,227 @@ import cytools as cyt import numpy as np -test_data_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/polytope_data.json') +test_data_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "data/polytope_data.json" +) + class Polytope_Sage_Comparison_Test(unittest.TestCase): def setUp(self): - with open(test_data_path,'r') as fp: + with open(test_data_path, "r") as fp: self.test_data = json.load(fp) def test_polytope_data(self): print("Testing: Polytope data...") + def points_to_indices(points_in, points): - pts_dict = {tuple(ii):i for i,ii in enumerate(points)} + pts_dict = {tuple(ii): i for i, ii in enumerate(points)} return [pts_dict[tuple(pt)] for pt in points_in] + try: from tqdm import tqdm + data_iter = tqdm(self.test_data) except: data_iter = self.test_data for doc in data_iter: - vertices = eval(doc['vertices']) + vertices = eval(doc["vertices"]) poly = cyt.Polytope(vertices) dual_poly = poly.dual() points = poly.points() dual_points = dual_poly.points() - sage_points = [list(i) for i in eval(doc['N_points'])] - sage_dual_points = [list(i) for i in eval(doc['M_points'])] - sage_favorable = eval(doc['N_favorable']) - sage_dual_favorable = eval(doc['M_favorable']) + sage_points = [list(i) for i in eval(doc["N_points"])] + sage_dual_points = [list(i) for i in eval(doc["M_points"])] + sage_favorable = eval(doc["N_favorable"]) + sage_dual_favorable = eval(doc["M_favorable"]) + + sage_faces_0_pts = [ + [sage_points[ii] for ii in i] for i in eval(doc["N_faces_0"]) + ] + sage_faces_1_pts = [ + [sage_points[ii] for ii in i] for i in eval(doc["N_faces_1"]) + ] + sage_faces_2_pts = [ + [sage_points[ii] for ii in i] for i in eval(doc["N_faces_2"]) + ] + sage_faces_3_pts = [ + [sage_points[ii] for ii in i] for i in eval(doc["N_faces_3"]) + ] + sage_dual_faces_0_pts = [ + [sage_dual_points[ii] for ii in i] for i in eval(doc["M_faces_0"]) + ] + sage_dual_faces_1_pts = [ + [sage_dual_points[ii] for ii in i] for i in eval(doc["M_faces_1"]) + ] + sage_dual_faces_2_pts = [ + [sage_dual_points[ii] for ii in i] for i in eval(doc["M_faces_2"]) + ] + sage_dual_faces_3_pts = [ + [sage_dual_points[ii] for ii in i] for i in eval(doc["M_faces_3"]) + ] + + sage_faces_0 = sorted( + [sorted(points_to_indices(f, points)) for f in sage_faces_0_pts] + ) + sage_faces_1 = sorted( + [sorted(points_to_indices(f, points)) for f in sage_faces_1_pts] + ) + sage_faces_2 = sorted( + [sorted(points_to_indices(f, points)) for f in sage_faces_2_pts] + ) + sage_faces_3 = sorted( + [sorted(points_to_indices(f, points)) for f in sage_faces_3_pts] + ) + sage_dual_faces_0 = sorted( + [ + sorted(points_to_indices(f, dual_points)) + for f in sage_dual_faces_0_pts + ] + ) + sage_dual_faces_1 = sorted( + [ + sorted(points_to_indices(f, dual_points)) + for f in sage_dual_faces_1_pts + ] + ) + sage_dual_faces_2 = sorted( + [ + sorted(points_to_indices(f, dual_points)) + for f in sage_dual_faces_2_pts + ] + ) + sage_dual_faces_3 = sorted( + [ + sorted(points_to_indices(f, dual_points)) + for f in sage_dual_faces_3_pts + ] + ) - sage_faces_0_pts = [[sage_points[ii] for ii in i] for i in eval(doc['N_faces_0'])] - sage_faces_1_pts = [[sage_points[ii] for ii in i] for i in eval(doc['N_faces_1'])] - sage_faces_2_pts = [[sage_points[ii] for ii in i] for i in eval(doc['N_faces_2'])] - sage_faces_3_pts = [[sage_points[ii] for ii in i] for i in eval(doc['N_faces_3'])] - sage_dual_faces_0_pts = [[sage_dual_points[ii] for ii in i] for i in eval(doc['M_faces_0'])] - sage_dual_faces_1_pts = [[sage_dual_points[ii] for ii in i] for i in eval(doc['M_faces_1'])] - sage_dual_faces_2_pts = [[sage_dual_points[ii] for ii in i] for i in eval(doc['M_faces_2'])] - sage_dual_faces_3_pts = [[sage_dual_points[ii] for ii in i] for i in eval(doc['M_faces_3'])] + faces_0 = sorted( + [sorted(poly.points_to_indices(f.points())) for f in poly.faces(0)] + ) + faces_1 = sorted( + [sorted(poly.points_to_indices(f.points())) for f in poly.faces(1)] + ) + faces_2 = sorted( + [sorted(poly.points_to_indices(f.points())) for f in poly.faces(2)] + ) + faces_3 = sorted( + [sorted(poly.points_to_indices(f.points())) for f in poly.faces(3)] + ) + dual_faces_0 = sorted( + [ + sorted(dual_poly.points_to_indices(f.dual().points())) + for f in poly.faces(0) + ] + ) + dual_faces_1 = sorted( + [ + sorted(dual_poly.points_to_indices(f.dual().points())) + for f in poly.faces(1) + ] + ) + dual_faces_2 = sorted( + [ + sorted(dual_poly.points_to_indices(f.dual().points())) + for f in poly.faces(2) + ] + ) + dual_faces_3 = sorted( + [ + sorted(dual_poly.points_to_indices(f.dual().points())) + for f in poly.faces(3) + ] + ) + favorable = poly.is_favorable(lattice="N") + dual_favorable = dual_poly.is_favorable(lattice="N") - sage_faces_0 = sorted([sorted(points_to_indices(f, points)) for f in sage_faces_0_pts]) - sage_faces_1 = sorted([sorted(points_to_indices(f, points)) for f in sage_faces_1_pts]) - sage_faces_2 = sorted([sorted(points_to_indices(f, points)) for f in sage_faces_2_pts]) - sage_faces_3 = sorted([sorted(points_to_indices(f, points)) for f in sage_faces_3_pts]) - sage_dual_faces_0 = sorted([sorted(points_to_indices(f, dual_points)) for f in sage_dual_faces_0_pts]) - sage_dual_faces_1 = sorted([sorted(points_to_indices(f, dual_points)) for f in sage_dual_faces_1_pts]) - sage_dual_faces_2 = sorted([sorted(points_to_indices(f, dual_points)) for f in sage_dual_faces_2_pts]) - sage_dual_faces_3 = sorted([sorted(points_to_indices(f, dual_points)) for f in sage_dual_faces_3_pts]) - - faces_0 = sorted([sorted(poly.points_to_indices(f.points())) for f in poly.faces(0)]) - faces_1 = sorted([sorted(poly.points_to_indices(f.points())) for f in poly.faces(1)]) - faces_2 = sorted([sorted(poly.points_to_indices(f.points())) for f in poly.faces(2)]) - faces_3 = sorted([sorted(poly.points_to_indices(f.points())) for f in poly.faces(3)]) - dual_faces_0 = sorted([sorted(dual_poly.points_to_indices(f.dual().points())) for f in poly.faces(0)]) - dual_faces_1 = sorted([sorted(dual_poly.points_to_indices(f.dual().points())) for f in poly.faces(1)]) - dual_faces_2 = sorted([sorted(dual_poly.points_to_indices(f.dual().points())) for f in poly.faces(2)]) - dual_faces_3 = sorted([sorted(dual_poly.points_to_indices(f.dual().points())) for f in poly.faces(3)]) - favorable = poly.is_favorable(lattice='N') - dual_favorable = dual_poly.is_favorable(lattice='N') + self.assertTrue( + sorted(points.tolist()) == sorted(sage_points), + msg="Polytope points don't match. \n Sage: \n" + + str(sage_points) + + "\n Cytools: \n" + + str(points), + ) + self.assertTrue( + sorted(dual_points.tolist()) == sorted(sage_dual_points), + msg="Dual polytope points don't match. \n Sage: \n" + + str(sage_dual_points) + + "\n Cytools: \n" + + str(dual_points), + ) + self.assertTrue( + faces_0 == sage_faces_0, + msg="Vertices don't match. \n Sage: \n" + + str(sage_faces_0) + + "\n Cytools: \n" + + str(faces_0), + ) + self.assertTrue( + faces_1 == sage_faces_1, + msg="1-faces don't match. \n Sage: \n" + + str(sage_faces_1) + + "\n Cytools: \n" + + str(faces_1), + ) + self.assertTrue( + faces_2 == sage_faces_2, + msg="2-faces don't match. \n Sage: \n" + + str(sage_faces_2) + + "\n Cytools: \n" + + str(faces_2), + ) + self.assertTrue( + faces_3 == sage_faces_3, + msg="3-faces don't match. \n Sage: \n" + + str(sage_faces_3) + + "\n Cytools: \n" + + str(faces_3), + ) + self.assertTrue( + dual_faces_0 == sage_dual_faces_0, + msg="Dual Vertices don't match. \n Sage: \n" + + str(sage_dual_faces_0) + + "\n Cytools: \n" + + str(dual_faces_0), + ) + self.assertTrue( + dual_faces_1 == sage_dual_faces_1, + msg="Dual 1-faces don't match. \n Sage: \n" + + str(sage_dual_faces_1) + + "\n Cytools: \n" + + str(dual_faces_1), + ) + self.assertTrue( + dual_faces_2 == sage_dual_faces_2, + msg="Dual 2-faces don't match. \n Sage: \n" + + str(sage_dual_faces_2) + + "\n Cytools: \n" + + str(dual_faces_2), + ) + self.assertTrue( + dual_faces_3 == sage_dual_faces_3, + msg="Dual 3-faces don't match. \n Sage: \n" + + str(sage_dual_faces_3) + + "\n Cytools: \n" + + str(dual_faces_3), + ) + self.assertTrue( + favorable == sage_favorable, + msg="Polytope favorability doesn't match. \n Sage: \n" + + str(sage_favorable) + + "\n Cytools: \n" + + str(favorable), + ) + self.assertTrue( + dual_favorable == sage_dual_favorable, + msg="Dual polytope favorability doesn't match. \n Sage: \n" + + str(sage_dual_favorable) + + "\n Cytools: \n" + + str(dual_favorable), + ) - self.assertTrue(sorted(points.tolist())==sorted(sage_points), - msg="Polytope points don't match. \n Sage: \n" + str(sage_points) - + "\n Cytools: \n" + str(points)) - self.assertTrue(sorted(dual_points.tolist())==sorted(sage_dual_points), - msg="Dual polytope points don't match. \n Sage: \n" + str(sage_dual_points) - + "\n Cytools: \n" + str(dual_points)) - self.assertTrue(faces_0==sage_faces_0, - msg="Vertices don't match. \n Sage: \n" + str(sage_faces_0) - + "\n Cytools: \n" + str(faces_0)) - self.assertTrue(faces_1==sage_faces_1, - msg="1-faces don't match. \n Sage: \n" + str(sage_faces_1) - + "\n Cytools: \n" + str(faces_1)) - self.assertTrue(faces_2==sage_faces_2, - msg="2-faces don't match. \n Sage: \n" + str(sage_faces_2) - + "\n Cytools: \n" + str(faces_2)) - self.assertTrue(faces_3==sage_faces_3, - msg="3-faces don't match. \n Sage: \n" + str(sage_faces_3) - + "\n Cytools: \n" + str(faces_3)) - self.assertTrue(dual_faces_0==sage_dual_faces_0, - msg="Dual Vertices don't match. \n Sage: \n" + str(sage_dual_faces_0) - + "\n Cytools: \n" + str(dual_faces_0)) - self.assertTrue(dual_faces_1==sage_dual_faces_1, - msg="Dual 1-faces don't match. \n Sage: \n" + str(sage_dual_faces_1) - + "\n Cytools: \n" + str(dual_faces_1)) - self.assertTrue(dual_faces_2==sage_dual_faces_2, - msg="Dual 2-faces don't match. \n Sage: \n" + str(sage_dual_faces_2) - + "\n Cytools: \n" + str(dual_faces_2)) - self.assertTrue(dual_faces_3==sage_dual_faces_3, - msg="Dual 3-faces don't match. \n Sage: \n" + str(sage_dual_faces_3) - + "\n Cytools: \n" + str(dual_faces_3)) - self.assertTrue(favorable==sage_favorable, - msg="Polytope favorability doesn't match. \n Sage: \n" + str(sage_favorable) - + "\n Cytools: \n" + str(favorable)) - self.assertTrue(dual_favorable==sage_dual_favorable, - msg="Dual polytope favorability doesn't match. \n Sage: \n" + str(sage_dual_favorable) - + "\n Cytools: \n" + str(dual_favorable)) -if __name__ == '__main__': - unittest.main() \ No newline at end of file +if __name__ == "__main__": + unittest.main() diff --git a/unittests/test_glsm.py b/unittests/test_glsm.py index ec1a09d..7438fbe 100644 --- a/unittests/test_glsm.py +++ b/unittests/test_glsm.py @@ -5,26 +5,57 @@ import cytools as cyt import itertools -test_data_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/polytope_data.json') +test_data_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "data/polytope_data.json" +) -def compute_glsm_data(poly, dual_poly, include_origin, include_points_interior_to_facets, integral): - glsm = poly.glsm_charge_matrix(include_origin=include_origin, - include_points_interior_to_facets=include_points_interior_to_facets) - basis = poly.glsm_basis(include_origin=include_origin, - include_points_interior_to_facets=include_points_interior_to_facets, integral=integral) - lin_rels = poly.glsm_linear_relations(include_origin=include_origin, - include_points_interior_to_facets=include_points_interior_to_facets) - dual_glsm = dual_poly.glsm_charge_matrix(include_origin=include_origin, - include_points_interior_to_facets=include_points_interior_to_facets) - dual_basis = dual_poly.glsm_basis(include_origin=include_origin, - include_points_interior_to_facets=include_points_interior_to_facets, integral=integral) - dual_lin_rels = dual_poly.glsm_linear_relations(include_origin=include_origin, - include_points_interior_to_facets=include_points_interior_to_facets) - return {'glsm':glsm, 'basis':basis, 'linear_relations':lin_rels, - 'dual_glsm':dual_glsm, 'dual_basis':dual_basis, - 'dual_linear_relations':dual_lin_rels} -def compute_point_data(poly, dual_poly, include_origin, include_points_interior_to_facets): +def compute_glsm_data( + poly, + dual_poly, + include_origin, + include_points_interior_to_facets, + integral, +): + glsm = poly.glsm_charge_matrix( + include_origin=include_origin, + include_points_interior_to_facets=include_points_interior_to_facets, + ) + basis = poly.glsm_basis( + include_origin=include_origin, + include_points_interior_to_facets=include_points_interior_to_facets, + integral=integral, + ) + lin_rels = poly.glsm_linear_relations( + include_origin=include_origin, + include_points_interior_to_facets=include_points_interior_to_facets, + ) + dual_glsm = dual_poly.glsm_charge_matrix( + include_origin=include_origin, + include_points_interior_to_facets=include_points_interior_to_facets, + ) + dual_basis = dual_poly.glsm_basis( + include_origin=include_origin, + include_points_interior_to_facets=include_points_interior_to_facets, + integral=integral, + ) + dual_lin_rels = dual_poly.glsm_linear_relations( + include_origin=include_origin, + include_points_interior_to_facets=include_points_interior_to_facets, + ) + return { + "glsm": glsm, + "basis": basis, + "linear_relations": lin_rels, + "dual_glsm": dual_glsm, + "dual_basis": dual_basis, + "dual_linear_relations": dual_lin_rels, + } + + +def compute_point_data( + poly, dual_poly, include_origin, include_points_interior_to_facets +): if include_origin and include_points_interior_to_facets: poly_points = poly.points() dual_poly_points = dual_poly.points() @@ -39,52 +70,106 @@ def compute_point_data(poly, dual_poly, include_origin, include_points_interior_ dual_poly_points = dual_poly.boundary_points_not_interior_to_facets() else: raise - return {'poly_points': poly_points, 'dual_poly_points': dual_poly_points} - + return {"poly_points": poly_points, "dual_poly_points": dual_poly_points} + class GLSM_Consistency_Test(unittest.TestCase): def setUp(self): - with open(test_data_path,'r') as fp: + with open(test_data_path, "r") as fp: self.test_data = json.load(fp) - self.test_data = self.test_data[:40] + self.test_data[40:-5:20] + self.test_data[-5:] + self.test_data = ( + self.test_data[:40] + self.test_data[40:-5:20] + self.test_data[-5:] + ) def test_glsm(self): print("Testing: GLSM charge matrix...") try: from tqdm import tqdm + data_iter = tqdm(self.test_data) except: data_iter = self.test_data for doc in data_iter: - vertices = eval(doc['vertices']) + vertices = eval(doc["vertices"]) poly = cyt.Polytope(vertices) dual_poly = poly.dual() for opt in itertools.product((True, False), repeat=3): - glsm_data = compute_glsm_data(poly, dual_poly, include_origin=opt[0], - include_points_interior_to_facets=opt[1], integral=opt[2]) - point_data = compute_point_data(poly, dual_poly, include_origin=opt[0], - include_points_interior_to_facets=opt[1]) - self.assertTrue(np.all(np.dot(glsm_data['glsm'], - point_data['poly_points'])==0), + glsm_data = compute_glsm_data( + poly, + dual_poly, + include_origin=opt[0], + include_points_interior_to_facets=opt[1], + integral=opt[2], + ) + point_data = compute_point_data( + poly, + dual_poly, + include_origin=opt[0], + include_points_interior_to_facets=opt[1], + ) + self.assertTrue( + np.all(np.dot(glsm_data["glsm"], point_data["poly_points"]) == 0), msg="Incorrect GLSM charge matrix. Parameters:" - + str(opt) + "\n Vertices of the polytope:" + str(vertices)) - self.assertTrue(np.linalg.slogdet(glsm_data['glsm'].T[glsm_data['basis']].T)[1]>-10, + + str(opt) + + "\n Vertices of the polytope:" + + str(vertices), + ) + self.assertTrue( + np.linalg.slogdet(glsm_data["glsm"].T[glsm_data["basis"]].T)[1] + > -10, msg="Inconsistent GLSM basis. Parameters:" - + str(opt) + "\n Vertices of the polytope:" + str(vertices) - + "basis:" + str(glsm_data['basis'])) - self.assertTrue(np.all(np.dot(glsm_data['linear_relations'],glsm_data['glsm'].T)==0), + + str(opt) + + "\n Vertices of the polytope:" + + str(vertices) + + "basis:" + + str(glsm_data["basis"]), + ) + self.assertTrue( + np.all( + np.dot(glsm_data["linear_relations"], glsm_data["glsm"].T) == 0 + ), msg="Incorrect linear relations. Parameters:" - + str(opt) + "\n Vertices of the polytope:" + str(vertices)) - self.assertTrue(np.all(np.dot(glsm_data['dual_glsm'], - point_data['dual_poly_points'])==0), + + str(opt) + + "\n Vertices of the polytope:" + + str(vertices), + ) + self.assertTrue( + np.all( + np.dot( + glsm_data["dual_glsm"], + point_data["dual_poly_points"], + ) + == 0 + ), msg="Incorrect GLSM charge matrix of dual polytope. Parameters:" - + str(opt) + "\n Vertices of the original polytope:" + str(vertices)) - self.assertTrue(np.linalg.slogdet(glsm_data['dual_glsm'].T[glsm_data['dual_basis']].T)[1]>-10, + + str(opt) + + "\n Vertices of the original polytope:" + + str(vertices), + ) + self.assertTrue( + np.linalg.slogdet( + glsm_data["dual_glsm"].T[glsm_data["dual_basis"]].T + )[1] + > -10, msg="Inconsistent GLSM basis of dual polytope. Parameters:" - + str(opt) + "\n Vertices of the original polytope:" + str(vertices)) - self.assertTrue(np.all(np.dot(glsm_data['dual_linear_relations'],glsm_data['dual_glsm'].T)==0), + + str(opt) + + "\n Vertices of the original polytope:" + + str(vertices), + ) + self.assertTrue( + np.all( + np.dot( + glsm_data["dual_linear_relations"], + glsm_data["dual_glsm"].T, + ) + == 0 + ), msg="Incorrect linear relations of dual polytope. Parameters:" - + str(opt) + "\n Vertices of the original polytope:" + str(vertices)) + + str(opt) + + "\n Vertices of the original polytope:" + + str(vertices), + ) + -if __name__ == '__main__': - unittest.main() \ No newline at end of file +if __name__ == "__main__": + unittest.main() diff --git a/unittests/test_kahler_positivity.py b/unittests/test_kahler_positivity.py index 80906eb..accef2f 100644 --- a/unittests/test_kahler_positivity.py +++ b/unittests/test_kahler_positivity.py @@ -5,7 +5,10 @@ from cytools import config import numpy as np -test_data_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/polytope_data.json') +test_data_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "data/polytope_data.json" +) + def compute_positivity_data(poly, triangulation_backend, intnum_backend): frst = poly.triangulate(backend=triangulation_backend, make_star=True) @@ -17,39 +20,62 @@ def compute_positivity_data(poly, triangulation_backend, intnum_backend): div_vols = cy.compute_divisor_volumes(kcone_tip, in_basis=False) Kinv = cy.compute_inverse_kahler_metric(kcone_tip) Kinv_eigvals = np.linalg.eigvals(Kinv) - K_eigvals = sorted([1/eig for eig in Kinv_eigvals]) - return {'CY_Volume':XVol, 'Divisor_Volumes':div_vols, 'Kahler_eigenvalues':K_eigvals} + K_eigvals = sorted([1 / eig for eig in Kinv_eigvals]) + return { + "CY_Volume": XVol, + "Divisor_Volumes": div_vols, + "Kahler_eigenvalues": K_eigvals, + } + class Kahler_Positivity_Test(unittest.TestCase): def setUp(self): - with open(test_data_path,'r') as fp: + with open(test_data_path, "r") as fp: self.test_data = json.load(fp) - self.test_data = self.test_data[:40] + self.test_data[40:-2:20] + self.test_data[-2:] + self.test_data = ( + self.test_data[:40] + self.test_data[40:-2:20] + self.test_data[-2:] + ) def test_positivity_cgal(self): try: from tqdm import tqdm + data_iter = tqdm(self.test_data) except: data_iter = self.test_data for doc in data_iter: - if doc['N_favorable']=="True": - vertices = eval(doc['vertices']) + if doc["N_favorable"] == "True": + vertices = eval(doc["vertices"]) poly = cyt.Polytope(vertices) - options = ((p,pp) for p in ('qhull', 'cgal', 'topcom') for pp in ('all', 'sksparse', 'scipy')) + options = ( + (p, pp) + for p in ("qhull", "cgal", "topcom") + for pp in ("all", "sksparse", "scipy") + ) for opt in options: positivity_data = compute_positivity_data(poly, opt[0], opt[1]) - self.assertTrue(positivity_data['CY_Volume']>0, - msg="Negative CY Volume. Options:" + str(opt) + - ": \n" + str(positivity_data['CY_Volume'])) - self.assertTrue(all(i>0 for i in positivity_data['Divisor_Volumes']), - msg="Negative divisor volumes. Options:" + str(opt) + - ": \n" + str(positivity_data['Divisor_Volumes'])) - self.assertTrue(all(i>0 for i in positivity_data['Kahler_eigenvalues']), - msg="Negative Kahler metric eigenvalues. Options:" + str(opt) + - ": \n" + str(positivity_data['Kahler_eigenvalues'])) - -if __name__ == '__main__': - unittest.main() - - \ No newline at end of file + self.assertTrue( + positivity_data["CY_Volume"] > 0, + msg="Negative CY Volume. Options:" + + str(opt) + + ": \n" + + str(positivity_data["CY_Volume"]), + ) + self.assertTrue( + all(i > 0 for i in positivity_data["Divisor_Volumes"]), + msg="Negative divisor volumes. Options:" + + str(opt) + + ": \n" + + str(positivity_data["Divisor_Volumes"]), + ) + self.assertTrue( + all(i > 0 for i in positivity_data["Kahler_eigenvalues"]), + msg="Negative Kahler metric eigenvalues. Options:" + + str(opt) + + ": \n" + + str(positivity_data["Kahler_eigenvalues"]), + ) + + +if __name__ == "__main__": + unittest.main()