regfans 0.0.3__py3-none-any.whl → 0.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
regfans/fan.py CHANGED
@@ -45,7 +45,7 @@ class Fan:
45
45
  function.
46
46
 
47
47
  This class is *not* intended to be called directly. Instead, it is meant to
48
- be called through VectorConfiguration.subdivide.
48
+ be called through VectorConfiguration.triangulate.
49
49
 
50
50
  **Arguments:**
51
51
  - `vc`: The ambient vector configuration that this fan is over.
@@ -139,8 +139,8 @@ class Fan:
139
139
  f"A "
140
140
  + fine_str
141
141
  + regular_str
142
- + f" {subdivision_str} of "
143
- + repr(self._vc)
142
+ + f" {subdivision_str} of a"
143
+ + repr(self._vc)[1:]
144
144
  )
145
145
 
146
146
  def __str__(self) -> str:
@@ -171,8 +171,8 @@ class Fan:
171
171
  f"A "
172
172
  + fine_str
173
173
  + regular_str
174
- + f" {subdivision_str} of "
175
- + str(self._vc)
174
+ + f" {subdivision_str} of a"
175
+ + str(self._vc)[1:]
176
176
  )
177
177
 
178
178
  def __hash__(self) -> int:
@@ -417,7 +417,7 @@ class Fan:
417
417
  """
418
418
  if not self.is_triangulation():
419
419
  # the following assumes simplicial cones
420
- raise NotImplementedError
420
+ raise NotImplementedError("Not implemented for non-triangulations")
421
421
 
422
422
  # compute the facets as a map from facet labels to containing cones
423
423
  facets = dict()
@@ -534,7 +534,7 @@ class Fan:
534
534
  """
535
535
  **Description:**
536
536
  Return whether or not the fan also defines a (star) subdivision of the
537
- underlying point configuration.
537
+ original underlying point configuration.
538
538
 
539
539
  **Arguments:**
540
540
  None.
@@ -545,7 +545,7 @@ class Fan:
545
545
  """
546
546
  if not self.is_regular():
547
547
  # could be checked by MaxMP but I have't implemented that...
548
- raise NotImplementedError
548
+ raise NotImplementedError("Not implemented for irregular fans")
549
549
 
550
550
  # just check if central subdivision is a refinement
551
551
  H = self.secondary_cone_hyperplanes()
@@ -1040,7 +1040,7 @@ class Fan:
1040
1040
  # return
1041
1041
  if formal:
1042
1042
  # update neighb from being a tuple of cones to a formal Fan object
1043
- neighb = self.vc.subdivide(cells=neighb)
1043
+ neighb = self.vc.triangulate(cells=neighb)
1044
1044
 
1045
1045
  # pass along circuit info to the neighbor
1046
1046
  neighb._circuits = self._circuits.copy()
@@ -1232,8 +1232,8 @@ class Fan:
1232
1232
  else:
1233
1233
  assert h_target is None
1234
1234
  if verbosity >= 0:
1235
- print("DIRECTIONS ARE HANDLED IN A VERY BAD WAY... ")
1236
- print("h_target = h_init+1000*direction...")
1235
+ print("(Warning: directions are handled in a sub-optimal way...)")
1236
+ print("( one sets h_target = h_init+1000*direction... )")
1237
1237
  direction = 1000*np.array(direction)
1238
1238
  h_target = h_init+direction
1239
1239
  direction_norm2 = np.dot(direction,direction)
@@ -1678,13 +1678,13 @@ def flip_subgraph(
1678
1678
  tri_init = seed
1679
1679
  labels = [
1680
1680
  {
1681
- "reg": tri_init.is_regular(),
1681
+ "regular": tri_init.is_regular(),
1682
1682
  "fine": tri_init.is_fine(),
1683
- "triang": tri_init.respects_ptconfig(),
1683
+ "respects_ptconfig": tri_init.respects_ptconfig(),
1684
1684
  }
1685
1685
  ]
1686
1686
  else:
1687
- tri_init = seed.subdivide()
1687
+ tri_init = seed.triangulate()
1688
1688
  if not tri_init.is_triangulation():
1689
1689
  print("ERROR: seed isn't triangulation! Quitting!")
1690
1690
  return
@@ -1697,9 +1697,9 @@ def flip_subgraph(
1697
1697
 
1698
1698
  labels = [
1699
1699
  {
1700
- "reg": True,
1700
+ "regular": True,
1701
1701
  "fine": tri_init.is_fine(),
1702
- "triang": True,
1702
+ "respects_ptconfig": True,
1703
1703
  }
1704
1704
  ]
1705
1705
 
@@ -1751,17 +1751,17 @@ def flip_subgraph(
1751
1751
  continue
1752
1752
 
1753
1753
  # check various user-imposed restrictions
1754
- new_label = {"reg": None, "fine": None, "triang": None}
1754
+ new_label = {"regular": None, "fine": None, "respects_ptconfig": None}
1755
1755
  if compute_node_labels:
1756
1756
  # compute regularity (/maybe check it)
1757
1757
  # ------------------
1758
1758
  if verbosity >= 2:
1759
1759
  print("checking regularity...", end=" ")
1760
- new_label["reg"] = neighb.is_regular()
1760
+ new_label["regular"] = neighb.is_regular()
1761
1761
  if verbosity >= 2:
1762
- print(new_label["reg"])
1762
+ print(new_label["regular"])
1763
1763
 
1764
- if only_regular and not new_label["reg"]:
1764
+ if only_regular and not new_label["regular"]:
1765
1765
  continue
1766
1766
 
1767
1767
  # compute fineness (/maybe check it)
@@ -1779,25 +1779,25 @@ def flip_subgraph(
1779
1779
  # -----------------
1780
1780
  if verbosity >= 2:
1781
1781
  print("checking PC triangulation...", end=" ")
1782
- new_label["triang"] = neighb.respects_ptconfig()
1782
+ new_label["respects_ptconfig"] = neighb.respects_ptconfig()
1783
1783
  if verbosity >= 2:
1784
- print(new_label["triang"])
1784
+ print(new_label["respects_ptconfig"])
1785
1785
 
1786
- if only_pc_triang and (not new_label["triang"]):
1786
+ if only_pc_triang and (not new_label["respects_ptconfig"]):
1787
1787
  continue
1788
1788
  else:
1789
1789
  # lazily compute the labels
1790
1790
  if only_regular:
1791
- new_label["reg"] = neighb.is_regular()
1792
- if not new_label["reg"]:
1791
+ new_label["regular"] = neighb.is_regular()
1792
+ if not new_label["regular"]:
1793
1793
  continue
1794
1794
  if only_fine:
1795
1795
  new_label["fine"] = neighb.is_fine()
1796
1796
  if not new_label["fine"]:
1797
1797
  continue
1798
1798
  if only_pc_triang:
1799
- new_label["triang"] = neighb.respects_ptconfig()
1800
- if not new_label["triang"]:
1799
+ new_label["respects_ptconfig"] = neighb.respects_ptconfig()
1800
+ if not new_label["respects_ptconfig"]:
1801
1801
  continue
1802
1802
 
1803
1803
  # this is a new triangulation!
regfans/util.py CHANGED
@@ -28,6 +28,74 @@ import numpy as np
28
28
  from ortools.linear_solver import pywraplp
29
29
  from typing import Union
30
30
 
31
+ # basic math
32
+ # ----------
33
+ def gcd(vals: list[float], max_denom: float=10**6) -> float:
34
+ """
35
+ **Description:**
36
+ Computes the 'GCD' of a collection of floating point numbers.
37
+ This is the smallest number, g, such that g*values is integral.
38
+
39
+ This is computed by
40
+ 1) converting `values` to be rational [n0/d0, n1/d1, ...],
41
+ 2) computing the LCM, l, of [d0, d1, ...],
42
+ 3) computing the GCD, g', of [l*n0/d0, l*n1/d1, ...], and then
43
+ 4) returning g=g'/l.
44
+
45
+ **Arguments:**
46
+ - `vals`: The numbers to compute the GCD of.
47
+ - `max_denom`: Assert |di| <= max_denom
48
+
49
+ **Returns:**
50
+ The minimum number g' such that g'*vals is integral.
51
+ """
52
+ # compute the rational representation
53
+ rat = [fractions.Fraction(v).limit_denominator(max_denom) for v in vals]
54
+ numer = [r.numerator for r in rat]
55
+ denom = [r.denominator for r in rat]
56
+
57
+ # get the relevant LCM, GCD
58
+ l = functools.reduce(math.lcm, denom)
59
+ gprime= functools.reduce(math.gcd, [n*(l//d) for n,d in zip(numer,denom)])
60
+
61
+ # return the GCD
62
+ if gprime%l == 0:
63
+ # integral
64
+ return gprime//l
65
+ else:
66
+ return gprime/l
67
+
68
+ def primitive(vec: list[float], max_denom=10**10):
69
+ """
70
+ **Description:**
71
+ Computes the primitive vector associated to the input ray {c*vec: c>=0}.
72
+ Very similar to the gcd function.
73
+
74
+ This is equivalent to
75
+ vec/gcd(vec)
76
+ but just uses a rational representation.
77
+
78
+ **Arguments:**
79
+ - `vec`: A vector defining the ray {c*vec: c>=0}
80
+ - `max_denom`: Assert |di| <= max_denom
81
+
82
+ **Returns:**
83
+ The primitive vector along the ray.
84
+ """
85
+ # compute the rational representation
86
+ rat = [fractions.Fraction(v).limit_denominator(max_denom) for v in vec]
87
+ numer = [r.numerator for r in rat]
88
+ denom = [r.denominator for r in rat]
89
+
90
+ # get the LCM of the denominators
91
+ l = functools.reduce(math.lcm, denom)
92
+
93
+ # get the integral vector and scale it to be primitive
94
+ prim = [n*(l//d) for n,d in zip(numer,denom)]
95
+ gprime= functools.reduce(math.gcd, prim)
96
+
97
+ return [x//gprime for x in prim]
98
+
31
99
  # basic geometry
32
100
  # --------------
33
101
  def lerp(p0: "ArrayLike", p1: "ArrayLike", t: float) -> "ArrayLike":
@@ -365,72 +433,3 @@ def find_interior_point(*,
365
433
  else:
366
434
  warnings.warn(f"Unexpected error")
367
435
  return None
368
-
369
-
370
- # basic math - UNUSED
371
- # ----------
372
- def gcd(vals: list[float], max_denom: float=10**6) -> float:
373
- """
374
- **Description:**
375
- Computes the 'GCD' of a collection of floating point numbers.
376
- This is the smallest number, g, such that g*values is integral.
377
-
378
- This is computed by
379
- 1) converting `values` to be rational [n0/d0, n1/d1, ...],
380
- 2) computing the LCM, l, of [d0, d1, ...],
381
- 3) computing the GCD, g', of [l*n0/d0, l*n1/d1, ...], and then
382
- 4) returning g=g'/l.
383
-
384
- **Arguments:**
385
- - `vals`: The numbers to compute the GCD of.
386
- - `max_denom`: Assert |di| <= max_denom
387
-
388
- **Returns:**
389
- The minimum number g' such that g'*vals is integral.
390
- """
391
- # compute the rational representation
392
- rat = [fractions.Fraction(v).limit_denominator(max_denom) for v in vals]
393
- numer = [r.numerator for r in rat]
394
- denom = [r.denominator for r in rat]
395
-
396
- # get the relevant LCM, GCD
397
- l = functools.reduce(math.lcm, denom)
398
- gprime= functools.reduce(math.gcd, [n*(l//d) for n,d in zip(numer,denom)])
399
-
400
- # return the GCD
401
- if gprime%l == 0:
402
- # integral
403
- return gprime//l
404
- else:
405
- return gprime/l
406
-
407
- def primitive(vec: list[float], max_denom=10**10):
408
- """
409
- **Description:**
410
- Computes the primitive vector associated to the input ray {c*vec: c>=0}.
411
- Very similar to the gcd function.
412
-
413
- This is equivalent to
414
- vec/gcd(vec)
415
- but just uses a rational representation.
416
-
417
- **Arguments:**
418
- - `vec`: A vector defining the ray {c*vec: c>=0}
419
- - `max_denom`: Assert |di| <= max_denom
420
-
421
- **Returns:**
422
- The primitive vector along the ray.
423
- """
424
- # compute the rational representation
425
- rat = [fractions.Fraction(v).limit_denominator(max_denom) for v in vec]
426
- numer = [r.numerator for r in rat]
427
- denom = [r.denominator for r in rat]
428
-
429
- # get the LCM of the denominators
430
- l = functools.reduce(math.lcm, denom)
431
-
432
- # get the integral vector and scale it to be primitive
433
- prim = [n*(l//d) for n,d in zip(numer,denom)]
434
- gprime= functools.reduce(math.gcd, prim)
435
-
436
- return [x//gprime for x in prim]
regfans/vectorconfig.py CHANGED
@@ -29,6 +29,7 @@ import networkx as nx
29
29
  import numpy as np
30
30
  import scipy as sp
31
31
  import triangulumancer
32
+ import warnings
32
33
 
33
34
  # local imports
34
35
  from . import util, circuits, fan
@@ -490,7 +491,7 @@ class VectorConfiguration:
490
491
  # could definitely be generalized to non-solid
491
492
  # likely just check if
492
493
  # len(dual_cone(self.vectors())) == 2*(ambient-dim)
493
- raise NotImplementedError
494
+ raise NotImplementedError("Not implemented for non-solid VCs")
494
495
 
495
496
  return len(util.dual_cone(self.vectors())) == 0
496
497
 
@@ -677,12 +678,13 @@ class VectorConfiguration:
677
678
 
678
679
  # generating triangulations
679
680
  # =========================
680
- def subdivide(
681
+ def triangulate(
681
682
  self,
682
683
  heights: "ArrayLike" = None,
683
684
  cells: "ArrayLike" = None,
684
685
  tol: float = 1e-14,
685
- seed: int = 0,
686
+ backend: str = None,
687
+ check_heights: bool = True,
686
688
  verbosity: int = 0,
687
689
  ) -> "Fan":
688
690
  """
@@ -691,11 +693,14 @@ class VectorConfiguration:
691
693
  or by heights.
692
694
 
693
695
  **Arguments:**
694
- - `heights`: The heights to lift the vectors by.
695
- - `cells`: The cells to use in the triangulation.
696
- - `backend`: The lifting backend. Use 'qhull'.
697
- - `tol`: Numerical tolerance used.
698
- - `verbosity`: The verbosity level. Higher is more verbose
696
+ - `heights`: The heights to lift the vectors by.
697
+ - `cells`: The cells to use in the triangulation.
698
+ - `tol`: Numerical tolerance used for curing negative heights
699
+ - `backend`: The lifting backend. Currently allowed to be "cgal"
700
+ or "ppl".
701
+ - `check_heights`: Whether to check that the heights land in the
702
+ secondary cone of the output triangulation.
703
+ - `verbosity`: The verbosity level. Higher is more verbose
699
704
 
700
705
  **Returns:**
701
706
  The resultant subdivision.
@@ -709,13 +714,43 @@ class VectorConfiguration:
709
714
 
710
715
  # triangulate via heights
711
716
  # =======================
717
+ # flag as to whether the user didn't input heights
718
+ default_heights = (heights is None)
719
+
720
+ # check the backend
721
+ if backend is None:
722
+ if default_heights:
723
+ backend = "cgal"
724
+ else:
725
+ backend = "ppl"
726
+ else:
727
+ backend = backend.lower()
728
+ if backend not in ["cgal", "ppl"]:
729
+ raise ValueError(f"Unrecognized backend '{backend}'...")
730
+
731
+ # warning in case the user request PPL backend but gave no heights
732
+ if default_heights and (backend != "cgal"):
733
+ msg = "Non-cgal backends are not trustworthy for Deulaunay... "
734
+ msg += f"changing from '{backend}' to cgal..."
735
+ with warnings.catch_warnings():
736
+ warnings.simplefilter("always")
737
+ warnings.warn(msg)
738
+ backend = "cgal"
739
+
740
+ # allow small perturbations to the height of the origin to enforce that
741
+ # cgal gives a star triangulation
742
+ if backend == "cgal":
743
+ make_cgal_star = default_heights
744
+
712
745
  # (if no heights are provided, compute Delaunay triangulation)
713
746
  # (need to add noise to ensure it is a *triangulation* and not a
714
747
  # subdivision)
715
748
  # (allow retrying in case the noise brings the heights outside the
716
749
  # secondary fan... exceedingly unlikely though)
717
- if heights is None:
750
+ if default_heights:
718
751
  heights = np.sum(self.vectors()*self.vectors(), axis=1)
752
+ else:
753
+ heights = np.array(heights)
719
754
 
720
755
  # ensure the heights are non-negative
721
756
  already_nonneg = all([h_i >= 0 for h_i in heights])
@@ -729,7 +764,7 @@ class VectorConfiguration:
729
764
  heights_new, res = sp.optimize.nnls(B.T, Bh)
730
765
  if res>tol:
731
766
  print(f"Residuals {res} > tol {tol}...")
732
- raise ValueError
767
+ raise ValueError("Invalid heights")
733
768
 
734
769
  # do the check
735
770
  if False: # too slow... instead just check if we found coeffs...
@@ -762,37 +797,98 @@ class VectorConfiguration:
762
797
  return self.subdivide(cells=[self.labels])
763
798
 
764
799
  # nonzero heights -> lift via a point configuration
765
- pts = np.vstack( [np.zeros((1,self.dim), dtype=int), self.vectors()] )
766
- pc = triangulumancer.PointConfiguration(pts)
800
+ if backend == "cgal":
801
+ orig = np.zeros((1,self.ambient_dim),dtype=int)
802
+ pts = np.vstack([orig, self.vectors()])
803
+ pc = triangulumancer.PointConfiguration(pts)
804
+
805
+ if make_cgal_star:
806
+ # adjust heights for PC such that the triangulation is star...
807
+ # just ensure that 0 is in all simplices
808
+ #
809
+ # this should always be true by construction...
810
+ # but CGAL definitely perturbs heights a bit (e.g., lifting by
811
+ # heights = 0 doesn't lead to trivial subdivision)
812
+ #
813
+ # maybe this causes errors leading to non-star triangulations...
814
+ height_min = np.min(heights)
815
+ height_orig = -1e-6*height_min
816
+ while True:
817
+ heights_pc = np.concatenate(([height_orig], heights))
818
+ simp_pcinds = pc.triangulate_with_heights(heights_pc).simplices()
819
+
820
+ # lower the height of the origin if not star
821
+ if not all([0 in simp for simp in simp_pcinds]):
822
+ height_orig -= height_min
823
+ continue
824
+
825
+ # star :)
826
+ break
827
+
828
+ # check that we didn't lower the height of origin a crazy amount
829
+ if (verbosity >= 1) and (height_orig < -np.min(heights)):
830
+ msg = "Significantly lowered the height of the origin... "
831
+ msg += "maybe something went wrong..."
832
+ with warnings.catch_warnings():
833
+ warnings.simplefilter("always")
834
+ warnings.warn(msg)
835
+ else:
836
+ heights_pc = np.concatenate(([0], heights))
837
+ simp_pcinds = pc.triangulate_with_heights(heights_pc).simplices()
767
838
 
768
- # adjust heights for PC such that the triangulation is star...
769
- # just ensure that 0 is in all simplices
770
- #
771
- # this should always be true by construction. Maybe perturbations of
772
- # the heights in the various backend for odd heights like those which
773
- # give subdivisions cause the origin to be skipped...
774
- height_norm = np.linalg.norm(heights)
775
- height_orig = -height_norm
776
- while True:
777
- heights_pc = np.concatenate(([height_orig], heights))
778
- simp_pcinds = pc.triangulate_with_heights(heights_pc).simplices()
779
-
780
- # lower the height of the origin if not star
781
- if not all([0 in simp for simp in simp_pcinds]):
782
- height_orig -= height_norm
783
- continue
839
+ # read the simplices as indices in the VC
840
+ if not all([0 in s for s in simp_pcinds]):
841
+ msg = "cgal didn't produce a star triangulation... "
842
+ msg += f"cells = {simp_pcinds} (0 corresponds to origin). "
843
+ msg += "maybe try PPL..."
844
+ raise ValueError(msg)
845
+ simp_vcinds = [[pti-1 for pti in s if pti!=0] for s in simp_pcinds]
784
846
 
785
- # star :)
786
- break
847
+ elif backend == "ppl":
848
+ # construct the rays of the lifted cone
849
+ lifted = np.hstack([heights.reshape(-1,1),self.vectors()])
850
+ lifted = np.array([util.primitive(v) for v in lifted]) # as integers
851
+
852
+ H = np.array(util.dual_cone(lifted))
853
+ satd = H@lifted.T
854
+
855
+ # read the simplices as indices in the VC
856
+ simp_vcinds = [np.where(facet==0)[0].tolist() for facet in satd]
787
857
 
788
858
  # read the simplices as labels
789
- simp_vcinds = [[pti-1 for pti in s if pti!=0] for s in simp_pcinds]
790
859
  simp_labels = [[self.labels[vci] for vci in s] for s in simp_vcinds]
860
+ simp_labels = [sorted(simp) for simp in simp_labels]
861
+
862
+ # construct the fan
863
+ f = self.triangulate(cells=simp_labels)
791
864
 
792
- return self.subdivide(cells=simp_labels)
865
+ # some sanity checks
866
+ if not f.is_triangulation():
867
+ if verbosity >= 1:
868
+ msg = "Upon lifting, a non-triangulation subdivision was "
869
+ msg += f"output... (cells = {f.simplices()}) "
870
+ msg += "double check with another backend (PPL is preferable) "
871
+ msg += "OR perturb heights..."
872
+
873
+ with warnings.catch_warnings():
874
+ warnings.simplefilter("always")
875
+ warnings.warn(msg)
876
+ else:
877
+ # yes a triangulation...
878
+ # verify that the secondary cone contains the heights
879
+ if (not default_heights) and check_heights:
880
+ H = np.array(f.secondary_cone_hyperplanes())
881
+ dists = H@heights
882
+
883
+ if np.any((dists)<=0):
884
+ msg = "Heights not contained in secondary cone... "
885
+ msg += f"distances = {dists}..."
886
+ raise Exception(msg)
887
+
888
+ return f
793
889
 
794
890
  # aliases
795
- triangulate = subdivide
891
+ subdivide = triangulate
796
892
 
797
893
  def all_triangulations(
798
894
  self,
@@ -846,7 +942,8 @@ class VectorConfiguration:
846
942
  N: int = None,
847
943
  as_list: bool = False,
848
944
  attempts_per_triang: int = 1000,
849
- backend: str = "qhull",
945
+ backend: str = None,
946
+ seed: int = 0,
850
947
  verbosity: int = 0,
851
948
  ) -> Generator["Fan"] | list["Fan"]:
852
949
  """
@@ -871,7 +968,9 @@ class VectorConfiguration:
871
968
  or as a generator.
872
969
  - `attempts_per_triang`:Quit if we can't generate a new triangulation
873
970
  after this many tries.
874
- - `backend`: The lifting backend.
971
+ - `backend`: The lifting backend. See
972
+ `VectorConfiguration.triangulate`.
973
+ - `seed`: A random number seed.
875
974
  - `verbosity`: The verbosity level. Higher is more verbose.
876
975
 
877
976
  **Returns:**
@@ -933,6 +1032,7 @@ class VectorConfiguration:
933
1032
  iterator = range(N)
934
1033
 
935
1034
  # generate the triangulations
1035
+ np.random.seed(seed)
936
1036
  for _ in iterator:
937
1037
  if method == "delaunay":
938
1038
  # generate triangulations near Delaunay
@@ -953,7 +1053,7 @@ class VectorConfiguration:
953
1053
  h = np.multiply(h, self._vector_norms)
954
1054
 
955
1055
  try:
956
- t = self.subdivide(heights=h, backend=backend)
1056
+ t = self.triangulate(heights=h, backend=backend)
957
1057
  except sp.spatial.qhull.QhullError:
958
1058
  # QHull error :(
959
1059
  if verbosity >= 0:
@@ -1163,7 +1263,6 @@ class VectorConfiguration:
1163
1263
 
1164
1264
  def secondary_fan(self,
1165
1265
  only_fine: bool=False,
1166
- project_lineality: bool=False,
1167
1266
  formal_fan: bool=False,
1168
1267
  verbosity: int=0):
1169
1268
  """
@@ -1171,11 +1270,9 @@ class VectorConfiguration:
1171
1270
  Compute the secondary fan of the vector configuration.
1172
1271
 
1173
1272
  **Arguments:**
1174
- - `only_fine`: Restrict to fine triangulations.
1175
- - `project_lineality`: Project out lineality space with the gale
1176
- transform, mapping to the chamber complex.
1177
- - `formal_fan`: Save as a formal Fan object.
1178
- - `verbosity`: The verbosity level. Higher is more verbose
1273
+ - `only_fine`: Restrict to fine triangulations.
1274
+ - `formal_fan`: Save as a formal Fan object.
1275
+ - `verbosity`: The verbosity level. Higher is more verbose
1179
1276
 
1180
1277
  **Returns:**
1181
1278
  The secondary fan triangulations.
@@ -1185,19 +1282,21 @@ class VectorConfiguration:
1185
1282
  only_fine=only_fine,
1186
1283
  verbosity=verbosity)
1187
1284
 
1188
- # compute all of the secondary cones
1189
- fan = [fan.secondary_cone(project_lineality=project_lineality) for \
1190
- fan in triangs]
1285
+ # compute the (hyperplanes of the) secondary cones
1286
+ fan = [fan.secondary_cone_hyperplanes() for fan in triangs]
1191
1287
 
1192
1288
  # map to a formal fan
1193
1289
  if formal_fan:
1194
- rays = np.array(list(
1195
- {tuple(r) for cone in fan for r in cone.rays()}
1196
- ))
1197
- vc = VectorConfiguration(rays)
1290
+ fan_R = [util.dual_cone(H) for H in fan]
1291
+ all_rays = np.array(list({
1292
+ {tuple(r) for cone_R in fan for r in cone_R}
1293
+ }))
1294
+
1295
+ # construct the VC
1296
+ vc = VectorConfiguration(all_rays)
1198
1297
 
1199
- cones_as_labels = sorted([sorted(vc.vectors_to_labels(cone.rays()))\
1200
- for cone in fan])
1298
+ cones_as_labels = sorted([sorted(vc.vectors_to_labels(cone_R))\
1299
+ for cone_R in fan_R])
1201
1300
  fan = vc.subdivide(cells=cones_as_labels)
1202
1301
 
1203
1302
  return fan, triangs
@@ -1216,4 +1315,6 @@ class VectorConfiguration:
1216
1315
  **Returns:**
1217
1316
  The central fan.
1218
1317
  """
1219
- return self.subdivide(heights=[1 for _ in self.labels])
1318
+ return self.subdivide(
1319
+ heights=[1 for _ in self.labels],
1320
+ check_heights=False)
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: regfans
3
+ Version: 0.0.5
4
+ Summary: Regular fans of vector configurations
5
+ Author: Nate MacFadden
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Repository, https://github.com/natemacfadden/regfans
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: python-flint
14
+ Requires-Dist: ortools
15
+ Requires-Dist: networkx
16
+ Requires-Dist: numpy
17
+ Requires-Dist: scipy
18
+ Requires-Dist: triangulumancer
19
+ Provides-Extra: test
20
+ Requires-Dist: pplpy; extra == "test"
21
+ Requires-Dist: pytest; extra == "test"
22
+ Dynamic: license-file
23
+
24
+ ![Fan flip graph](images/fan_flip_graph.png)
25
+
26
+ # regfans
27
+ Software for studying vector configurations defined over the lattice vectors. This includes
28
+ - constructing regular triangulations (i.e., polyhedral fans) of such vector configurations via lifting,
29
+ - constructing all (regular) triangulations via computation of flip graphs,
30
+ - verification of various properties of the vector configuration/fan, and
31
+ - efficient linear flipping.
32
+
33
+ See [Triangulations: Structures for Algorithms and Applications](https://link.springer.com/book/10.1007/978-3-642-12971-1) by De Loera, Rambau, and Santos for a definitive resource on such topics.
34
+
35
+ This package, `regfans`, was originally developed for constructing toric varieties in the work [Calabi-Yau Threefolds from Vex Triangulations](https://arxiv.org/abs/2512.14817). Said work was supported in part by NSF grant PHY-2309456. All toric-geometric computations are isolated to [CYTools](https://github.com/LiamMcAllisterGroup/cytools), which has an extension `vector_config` building off of `regfans`.
36
+
37
+ ## Installation
38
+ `regfans` can be installed using either conda or pip. To install `regfans` using conda, please see/use the provided `environment.yml` file:
39
+ ```
40
+ conda env create -f environment.yml
41
+ conda activate regfans
42
+ ```
43
+ To install `regfans` using pip, either run (to install the most recent release; also see [PyPI listing](https://pypi.org/project/regfans/))
44
+ ```
45
+ pip install regfans
46
+ ```
47
+ or (to install a local version)
48
+ ```
49
+ pip install .
50
+ ```
51
+ N.B.: many methods in `regfans` require computation of dual cones (i.e., the generators of a cone defined via hyperplanes or vice-versa). Currently, this requires [pplpy](https://pypi.org/project/pplpy/) which cannot be automatically installed via pip.
52
+
53
+ ## API
54
+
55
+ See [api.md](documentation/api.md) for full API reference.
56
+
57
+ (To update documentation, just run `pydoc-markdown; py documentation/clean_api.py`)
58
+
59
+ ## Tutorials
60
+
61
+ See the [tutorials directory](tutorials/) for some commented example scripts showing how to construct a vector configuration, check properties of it, construct fans from it, and check the properties of said fans.
@@ -0,0 +1,10 @@
1
+ regfans/__init__.py,sha256=ySoKB1J2jeUD9oHoPqD2d-utSO-cqd9eI1dq4Q2SMBo,103
2
+ regfans/circuits.py,sha256=5uLH8oQiAKe7UYl7sKvFpxMzNViP9tcLtqA91zbWduk,11244
3
+ regfans/fan.py,sha256=w3VMzH1DxQq0qiC5Isi0vQUf3Ugc2C4mqcwrkgtGhyY,65043
4
+ regfans/util.py,sha256=JeyaFZ2a5ujQNoQAgKUim9zo_Ot_QZZ04YnQaQU1y4Y,12923
5
+ regfans/vectorconfig.py,sha256=F8UsQslI5GIX9N9eun3H8aNOcA84L_-Ok-HicY-q3Bw,45214
6
+ regfans-0.0.5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
7
+ regfans-0.0.5.dist-info/METADATA,sha256=fzmse5-x2yVA-CLlrCE01JatRbBoIa-SxVb3HMQeVhE,2797
8
+ regfans-0.0.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
9
+ regfans-0.0.5.dist-info/top_level.txt,sha256=h_8B8JgprCNKyinFEG8XvWOLKDouj_vmktFOf6_7Q0Q,8
10
+ regfans-0.0.5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,37 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: regfans
3
- Version: 0.0.3
4
- Summary: Regular fans of vector configurations
5
- Author: Nate MacFadden
6
- License-Expression: GPL-3.0-or-later
7
- Project-URL: Repository, https://github.com/natemacfadden/regfans
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.9
10
- Requires-Python: <3.14,>=3.9
11
- Description-Content-Type: text/markdown
12
- License-File: LICENSE
13
- Requires-Dist: python-flint
14
- Requires-Dist: ortools
15
- Requires-Dist: networkx
16
- Requires-Dist: numpy
17
- Requires-Dist: scipy
18
- Requires-Dist: triangulumancer
19
- Provides-Extra: test
20
- Requires-Dist: pplpy; extra == "test"
21
- Requires-Dist: pytest; extra == "test"
22
- Dynamic: license-file
23
-
24
- # vectorconfigurations
25
- Software for studying vector configurations and their triangulations. Originally developed for arXiv:2512.14817
26
-
27
- ## Installation
28
- Computation of dual cones requires pplpy. Install via conda:
29
- ```
30
- conda env create -f environment.yml
31
- conda activate regfans
32
- ```
33
- ## API Documentation
34
-
35
- See [API_DOC.md](API_DOC.md) for full API reference.
36
-
37
- (Developer note: update documentation simply by running `pydoc-markdown; py clean_API_DOC.py` in current directory)
@@ -1,10 +0,0 @@
1
- regfans/__init__.py,sha256=ySoKB1J2jeUD9oHoPqD2d-utSO-cqd9eI1dq4Q2SMBo,103
2
- regfans/circuits.py,sha256=5uLH8oQiAKe7UYl7sKvFpxMzNViP9tcLtqA91zbWduk,11244
3
- regfans/fan.py,sha256=dHLaDy00Qq0tWas6237vxyY88UiIo-4_3R7V7JllpY8,64783
4
- regfans/util.py,sha256=HdowruB_t37RWosu7XRVHjOdcxgyuxhR1YxdxUY1K94,12933
5
- regfans/vectorconfig.py,sha256=VUZpyzUjuMy9QCR3SayMwOE7Q-ve5V610kwQTDbl39Q,40932
6
- regfans-0.0.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
7
- regfans-0.0.3.dist-info/METADATA,sha256=roRs3mohUPcBGDq358QHR26BlCO_EiZ-4OPdtEit8Rk,1154
8
- regfans-0.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- regfans-0.0.3.dist-info/top_level.txt,sha256=h_8B8JgprCNKyinFEG8XvWOLKDouj_vmktFOf6_7Q0Q,8
10
- regfans-0.0.3.dist-info/RECORD,,