Source code for bigger.lamination

""" 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, 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]): """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: 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(f"{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 f"Infinitely supported lamination {self.describe(islice(self.support(), 10))} ..." return f"Lamination {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] @memoize() @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] @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 image = short_other.meeting(side.edge) # image is finitely supported. star_support = set(sidy.edge for triangle in image.supporting_triangles() for sidy in triangle) for edge in star_support: arc = inv_conjugator.source.edge_arc(edge) potential_unicorns.add(inv_conjugator(arc)) _, sequence = image.shorten() for i in range(1, len(sequence) + 1): prefix = sequence[:i] inv_prefix = ~prefix # Try to shrink the star support. move = sequence[-1] image = move(image) star_support = set(sidy.edge for triangle in image.supporting_triangles() for sidy in triangle) for edge in star_support: arc = inv_prefix.source.edge_arc(edge) arc_premove = (~move)(arc) # Only actually need to pull back the new edge, that is, the one for which ~move(arc) is not an edge. if [arc_premove(edgy) for edgy in arc_premove.support()] != [-1]: # arc_premove.support() is a set, so is unique. potential_unicorns.add(inv_conjugator(inv_prefix(arc))) 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)