""" A module for representing laminations on Triangulations. """
from __future__ import annotations
from collections import defaultdict, Counter
from collections.abc import Collection
from itertools import chain, islice
from typing import Any, Callable, Dict, Generic, Iterable, Union, Optional
from PIL import Image # type: ignore
import bigger
from bigger.types import Edge
from bigger.decorators import memoize, finite
[docs]class Lamination(Generic[Edge]): # pylint: disable=too-many-public-methods
"""A measured lamination on a :class:`~bigger.triangulation.Triangulation`.
The lamination is defined via a function mapping the edges of its underlying Triangulation to their corresponding measure."""
def __init__(self, triangulation: bigger.Triangulation[Edge], weight: Callable[[Edge], int], support: Callable[[], Iterable[Edge]]) -> None:
self.triangulation = triangulation
self.weight = weight
self.support = support
[docs] def supporting_sides(self) -> Iterable[bigger.Side[Edge]]:
"""Return the sides supporting this lamination."""
for edge in self.support():
for orientation in [True, False]:
yield bigger.Side(edge, orientation)
[docs] @finite
def supporting_triangles(self) -> set[bigger.triangulation.Triangle[Edge]]:
"""Return a set of triangles supporting this lamination, useful for debugging."""
# Note this could return an Iterable and the finite decorator removed, but that would probably be less useful for debugging.
return set(self.triangulation.triangle(side) for side in self.supporting_sides())
@memoize()
def __call__(self, edge: Union[Edge, bigger.Side[Edge]]) -> int:
if isinstance(edge, bigger.Side):
return self(edge.edge)
return self.weight(edge)
@finite
def __hash__(self) -> int:
return hash(frozenset((edge, self(edge)) for edge in self.support()))
@finite
def __bool__(self) -> bool:
return any(self(edge) for edge in self.support())
[docs] @memoize()
def dual(self, side: bigger.Side[Edge]) -> int:
"""Return the weight of this lamination dual to the given side."""
corner = self.triangulation.corner(side)
a, b, c = self(corner[0].edge), self(corner[1].edge), self(corner[2].edge)
af, bf, cf = max(a, 0), max(b, 0), max(c, 0) # Correct for negatives.
correction = min(af + bf - cf, bf + cf - af, cf + af - bf, 0)
return bigger.utilities.half(bf + cf - af + correction)
[docs] def left(self, side: bigger.Side[Edge]) -> int:
"""Return the dual weight to the left of this side."""
return self.dual(self.triangulation.right(side))
[docs] def right(self, side: bigger.Side[Edge]) -> int:
"""Return the dual weight to the right of this side."""
return self.dual(self.triangulation.left(side))
[docs] def describe(self, edges: Iterable[Edge]) -> str:
"""Return a string describing this Lamination on the given edges."""
return ", ".join("{}: {}".format(edge, self(edge)) for edge in edges)
[docs] def is_finitely_supported(self) -> bool:
"""Return whether this lamination is supported on finitely many edges of the underlying Triangulation."""
return isinstance(self.support(), Collection)
@finite
def __eq__(self, other: Any) -> bool:
if isinstance(other, Lamination):
if not other.is_finitely_supported():
raise ValueError("Equality testing requires finitely supported laminations")
return self.support() == other.support() and all(self(edge) == other(edge) for edge in self.support())
elif isinstance(other, dict):
return set(self.support()) == set(other) and all(self(edge) == other[edge] for edge in self.support())
return NotImplemented
def __str__(self) -> str:
if not self.is_finitely_supported():
return "Infinitely supported lamination {} ...".format(self.describe(islice(self.support(), 10)))
return "Lamination {}".format(self.describe(self.support()))
def __repr__(self) -> str:
return str(self)
def __add__(self, other: Lamination[Edge]) -> Lamination[Edge]:
"""Return the Haken sum of this lamination and another."""
def weight(edge: Edge) -> int:
return self(edge) + other(edge)
return self.triangulation(weight, lambda: chain(self.support(), other.support()), self.is_finitely_supported() and other.is_finitely_supported())
def __sub__(self, other: Lamination[Edge]) -> Lamination[Edge]:
def weight(edge: Edge) -> int:
return self(edge) - other(edge)
return self.triangulation(weight, lambda: chain(self.support(), other.support()), self.is_finitely_supported() and other.is_finitely_supported())
def __mul__(self, other: int) -> Lamination[Edge]:
def weight(edge: Edge) -> int:
return other * self(edge)
return self.triangulation(weight, self.support, self.is_finitely_supported())
def __rmul__(self, other: int) -> Lamination[Edge]:
return self * other
[docs] @finite
def complexity(self) -> int:
"""Return the number of intersections between this Lamination and its underlying Triangulation."""
return sum(max(self(edge), 0) for edge in self.support())
[docs] def trace(self, side: bigger.Side[Edge], intersection: int) -> Iterable[tuple[bigger.Side[Edge], int]]:
"""Yield the intersections of the triangulation run over by this lamination from a starting point.
The starting point is specified by a `Side` and how many intersections into that side."""
start = (side, intersection)
assert 0 <= intersection < self(side) # Sanity.
while True:
corner = self.triangulation.corner(~side)
x, y, z = corner
if intersection < self.dual(z): # Turn right.
side, intersection = y, intersection # pylint: disable=self-assigning-variable
elif self.dual(x) < 0 and self.dual(z) <= intersection < self.dual(z) - self.dual(x): # Terminate.
break
else: # Turn left.
side, intersection = z, self(z) - self(x) + intersection
yield side, intersection
if (side, intersection) == start:
break
[docs] def meeting(self, edge: Edge) -> Lamination[Edge]:
"""Return the sublamination of self meeting the given edge.
Note: self does not need to be finitely supported but the sublamination must be.
Unfortunately we have no way of knowing this in advance."""
num_intersections = self(edge)
start_side = bigger.Side(edge)
intersections = set(range(num_intersections))
hits: Dict[Edge, int] = defaultdict(int)
while intersections:
start_intersection = next(iter(intersections))
last = None
for side, intersection in self.trace(start_side, start_intersection):
last = (side, intersection)
hits[side.edge] += 1
if side == start_side:
intersections.remove(intersection)
elif side == ~start_side:
intersections.remove(num_intersections - 1 - intersection)
if last != (start_side, start_intersection):
# We terminated into a vertex, so we must explore the other direction too.
hits[start_side.edge] += 1
intersections.remove(start_intersection)
for side, intersection in self.trace(~start_side, num_intersections - 1 - start_intersection):
hits[side.edge] += 1
if side == start_side:
intersections.remove(intersection)
elif side == ~start_side:
intersections.remove(num_intersections - 1 - intersection)
return self.triangulation(hits)
[docs] @memoize()
@finite
def peripheral_components(self) -> dict[Lamination[Edge], tuple[int, list[bigger.Side[Edge]]]]:
"""Return a dictionary mapping component to (multiplicity, vertex) for each component of self that is peripheral around a vertex."""
seen = set()
components = dict()
for side in self.supporting_sides():
walk = []
current = side
while current not in seen and self(current) > 0:
seen.add(current)
walk.append(current)
current = ~self.triangulation.left(current)
if current == side: # We made it all the way around.
multiplicity = bigger.utilities.maximin([0], (self.left(side) for side in walk))
if multiplicity > 0:
component = self.triangulation(Counter(side.edge for side in walk))
components[component] = (multiplicity, walk)
break
return components
[docs] @finite
def parallel_components(self) -> dict[Lamination[Edge], tuple[int, bigger.Side[Edge], bool]]:
"""Return a dictionary mapping component to (multiplicity, side, is_arc) for each component of self that is parallel to an edge."""
components = dict()
sides = set(side for edge in self.support() for side in self.triangulation.star(bigger.Side(edge)))
for side in sides:
if self(side) > 0:
continue
if side.orientation: # Don't double count.
multiplicity = -self(side)
if multiplicity > 0:
components[self.triangulation.side_arc(side)] = (multiplicity, side, True)
around, twisting = float("inf"), float("inf")
for index, (sidey, nxt) in enumerate(bigger.utilities.lookahead(self.triangulation.walk_vertex(side), 1)):
value = self.left(sidey)
around = min(around, value) # Always shrink around.
if nxt == ~side:
if 0 <= around < twisting < float("inf") and self.left(side) == self.right(side) == around:
assert not isinstance(twisting, float)
assert not isinstance(around, float)
multiplicity = twisting - around
components[self.triangulation.side_curve(side)] = (multiplicity, side, False)
break
if index: # Only shrink twisting when it's not the first (or last) value.
twisting = min(twisting, value)
if around < 0 or twisting <= 0: # Terminate early.
break
return components
[docs] @memoize()
@finite
def components(self) -> dict[Lamination[Edge], int]:
"""Return a dictionary mapping components to their multiplicities."""
short, conjugator = self.shorten()
conjugator_inv = ~conjugator
components = dict()
for component, (multiplicity, _) in short.peripheral_components().items():
components[conjugator_inv(component)] = multiplicity
for component, (multiplicity, _, _) in short.parallel_components().items():
components[conjugator_inv(component)] = multiplicity
return components
[docs] @finite
def peripheral(self) -> Lamination[Edge]:
"""Return the lamination consisting of the peripheral components of this Lamination."""
return self.triangulation.disjoint_sum(dict((component, multiplicity) for component, (multiplicity, _) in self.peripheral_components().items()))
[docs] @finite
def is_short(self) -> bool:
"""Return whether this Lamination is short."""
return self == self.triangulation.disjoint_sum(dict((component, multiplicity) for component, (multiplicity, _, _) in self.parallel_components().items()))
[docs] @memoize()
@finite
def shorten(self) -> tuple[bigger.Lamination[Edge], bigger.Encoding[Edge]]: # pylint: disable=too-many-branches
"""Return an :class:`~bigger.encoding.Encoding` that maps self to a short lamination."""
def shorten_strategy(self: Lamination[Edge], side: bigger.Side[Edge]) -> bool:
"""Return whether flipping this side is a good idea."""
if not self.triangulation.is_flippable(side):
return False
ed, ad, bd = [self.dual(sidey) for sidey in self.triangulation.corner(side)]
return ed < 0 or (ed == 0 and ad > 0 and bd > 0) # Non-parallel arc.
peripheral = self.peripheral()
lamination = self - peripheral
conjugator = self.triangulation.identity()
arc_components, curve_components = dict(), dict()
while True:
# Subtract.
for component, (multiplicity, side, is_arc) in lamination.parallel_components().items():
lamination = lamination - component * multiplicity
if is_arc:
arc_components[side] = multiplicity
else: # is a curve.
curve_components[side] = multiplicity
if not lamination:
break
# The arcs will be dealt with in the first round and once they are gone, they are gone.
extra: Iterable[bigger.Side[Edge]] = [] # High priority edges to check.
while True:
try:
side = next(side for side in chain(extra, lamination.supporting_sides()) if shorten_strategy(lamination, side))
except StopIteration:
break
extra = lamination.triangulation.corner(~side)[1:]
move = lamination.triangulation.flip({side}) # side is always flippable.
conjugator = move * conjugator
lamination = move(lamination)
peripheral = move(peripheral)
# Now all arcs should be parallel to edges and there should now be no bipods.
assert all(lamination.left(side) >= 0 for side in lamination.supporting_sides())
assert all(sum(1 if lamination.left(side) > 0 else 0 for side in lamination.triangulation.triangle(side)) != 2 for side in lamination.supporting_sides())
used_sides = set()
hits: Dict[Edge, int] = defaultdict(int)
triangulation = lamination.triangulation
# Build a parallel multiarc. This is pretty inefficient.
for starting_side in lamination.supporting_sides():
if starting_side in used_sides or not lamination.left(starting_side):
continue
side = starting_side
add_sequence = False
while True: # Until we get back to the starting point.
used_sides.add(side)
if add_sequence: # Only record the edge in the sequence once we have made a right turn away from the vertex.
hits[side.edge] += 1
# Move around to the next edge following the lamination.
side = triangulation.left(~side) if lamination.left(~side) > 0 else triangulation.right(~side)
add_sequence = add_sequence or lamination.right(side) <= 0
if side == starting_side:
break
if hits:
multiarc = triangulation(hits)
# Recurse an use multiarc.shorten() now.
_, sub_conjugator = multiarc.shorten()
conjugator = sub_conjugator * conjugator
lamination = sub_conjugator(lamination)
peripheral = sub_conjugator(peripheral)
# Rebuild the image of self under conjugator from its components.
short = lamination.triangulation.disjoint_sum(
dict(
[(peripheral, 1)]
+ [(lamination.triangulation.side_arc(edge), multiplicity) for edge, multiplicity in arc_components.items()]
+ [(lamination.triangulation.side_curve(edge), multiplicity) for edge, multiplicity in curve_components.items()]
)
)
return short, conjugator
[docs] def twist(self, power: int = 1) -> bigger.Encoding[Edge]:
"""Return an :class:`~bigger.encoding.Encoding` that performs a Dehn twist about this Lamination.
Assumes but does not always check that this lamination is a multicurve."""
if self.is_finitely_supported():
short, conjugator = self.shorten()
twist = short.triangulation.identity()
for component, (multiplicity, side, is_arc) in short.parallel_components().items():
assert not is_arc
num_flips = component.complexity() - short.dual(side)
for _ in range(num_flips):
twist = twist.target.flip({twist.target.left(side)}) * twist
isom = dict()
x = y = side
while x != ~side:
isom[y] = x
x = ~twist.source.left(x)
y = ~twist.target.left(y)
twist = twist.target.relabel_from_dict(isom) * twist
twist = twist ** multiplicity
return (twist ** power).conjugate_by(conjugator)
def action(lamination: bigger.Lamination[Edge]) -> bigger.Lamination[Edge]:
def weight(edge: Edge) -> int:
# We used to do:
# return self.meeting(edge).twist(lamination, power)
# But by now using twisted_by we can get additional performance through memoization.
return lamination.twisted_by(self.meeting(edge), power=power)(edge)
def support() -> Iterable[Edge]:
for edge in lamination.support():
if weight(edge):
yield edge
for edgy in self.meeting(edge).support():
if weight(edgy):
yield edgy
return self.triangulation(weight, support, lamination.is_finitely_supported())
def inv_action(lamination: bigger.Lamination[Edge]) -> bigger.Lamination[Edge]:
def weight(edge: Edge) -> int:
return lamination.twisted_by(self.meeting(edge), power=-power)(edge)
def support() -> Iterable[Edge]:
for edge in lamination.support():
if weight(edge):
yield edge
for edgy in self.meeting(edge).support():
if weight(edgy):
yield edgy
return self.triangulation(weight, support, lamination.is_finitely_supported())
return bigger.Move(self.triangulation, self.triangulation, action, inv_action).encode()
[docs] @memoize()
def twisted_by(self, multicurve: Lamination[Edge], power: int = 1) -> Lamination[Edge]:
"""Return multicurve.twist()(self).
Assumes but does not check that multicurve is a multicurve.
This is used purely for performance by allowing for memoization in self.twist."""
return multicurve.twist(power)(self)
[docs] @finite
def intersection(self, *laminations: Lamination[Edge]) -> int:
"""Return the number of times that self intersects other."""
short, conjugator = self.shorten()
short_laminations = [conjugator(lamination) for lamination in laminations]
intersection = 0
# Peripheral components.
for _, (multiplicity, vertex) in short.peripheral_components().items():
for lamination in laminations:
intersection += multiplicity * sum(max(-lamination(edge), 0) + max(-lamination.left(edge), 0) for edge in vertex)
# Parallel components.
for _, (multiplicity, p, is_arc) in short.parallel_components().items():
if is_arc:
for short_lamination in short_laminations:
intersection += multiplicity * max(short_lamination(p), 0)
else: # is curve
walk = list(self.triangulation.walk_vertex(p))
v_edges = walk[:1] # The set of edges that come out of v from p round to ~p.
for short_lamination in short_laminations:
# There is probably a slick, one-pass way to get both around and out, like in self.parallel_components().
around = bigger.utilities.maximin([0], (short_lamination.left(edgy) for edgy in v_edges))
out = sum(max(-short_lamination.left(edge), 0) for edge in v_edges) + sum(max(-short_lamination(edge), 0) for edge in v_edges[1:])
assert min(around, out) == 0
intersection += multiplicity * (max(short_lamination(p), 0) - 2 * around + out)
return intersection
[docs] @finite
def unicorns(self, other: Lamination[Edge]) -> set[Lamination[Edge]]:
"""Return a set of arcs which contains all the unicorn arcs that can be made from self and other.
Note that it is possible that the set may also contain other non-unicorn arcs.
Assumes that self is a multiarc."""
short, conjugator = self.shorten()
inv_conjugator = ~conjugator
short_other = conjugator(other)
potential_unicorns = set()
for _, side, is_arc in short.parallel_components().values():
assert is_arc
restrict = short_other.meeting(side.edge) # restrict is finitely supported.
star_support = set(sidy.edge for triangle in restrict.supporting_triangles() for sidy in triangle)
for edge in star_support:
potential_unicorns.add(inv_conjugator(inv_conjugator.source.edge_arc(edge)))
image = restrict
_, sequence = restrict.shorten()
for i in range(len(sequence)):
move = sequence[~i]
prefix = sequence[:i]
inv_prefix = ~prefix
for edge in star_support:
arc = inv_prefix.source.edge_arc(edge)
# Only actually need to pull back the new edge, that is, the one for which ~move(arc) is not an edge.
potential_unicorns.add(inv_conjugator(inv_prefix(arc)))
# Try to shrink the star support.
image = move(image)
star_support = set(sidy.edge for triangle in image.supporting_triangles() for sidy in triangle)
return potential_unicorns
[docs] def draw(self, edges: Optional[list[Edge]] = None, **options: Any) -> Image:
"""Return a PIL image of this Lamination around the given edges."""
if edges is None:
if self.is_finitely_supported():
edges = list(self.support())
else:
raise ValueError("Edges must be specified for non-finitely supported laminations")
return bigger.draw(self, edges=edges, **options)