Skip to content

Index

pleat.conway

Conway topological operators for transforming tilings.

Re-exports the operator classes and factory functions from the submodules.

GeometricConwayOperator

GeometricConwayOperator(
    *super_args: object, **super_kwargs: object
)

Bases: TopologicalConwayOperator

Conway operator that assigns new vertex positions using barycentric coordinate interpolation.

Store the fundamental domain and convert its positions to barycentric coordinates.

After construction, every vertex in self.graph carries barycentric coordinates relative to the corner triangle (v1, vf, v2). They are re-projected to Euclidean coordinates per target triangle inside :meth:generate_graph_and_corners.

Source code in pleat/conway/operators.py
def __init__(self, *super_args: object, **super_kwargs: object) -> None:
    """Store the fundamental domain and convert its positions to barycentric coordinates.

    After construction, every vertex in ``self.graph`` carries barycentric
    coordinates relative to the corner triangle ``(v1, vf, v2)``. They are
    re-projected to Euclidean coordinates per target triangle inside
    :meth:`generate_graph_and_corners`.
    """
    super(GeometricConwayOperator, self).__init__(*super_args, **super_kwargs)
    # convert euclidean to barycentric coordinates
    to_barycentric = euclidean_to_barycentric_map(np.array([self.v1["pos"], self.vf["pos"], self.v2["pos"]]))
    for v in self.graph.vertices:
        v["pos"] = to_barycentric(v["pos"])
    self.geometry = None

get_tri

get_tri(h: HalfEdge) -> np.ndarray

Return the triangle (h.dest, face midpoint, h.orig) for half-edge h.

Source code in pleat/conway/operators.py
def get_tri(self, h: HalfEdge) -> np.ndarray:
    """Return the triangle ``(h.dest, face midpoint, h.orig)`` for half-edge ``h``."""
    midpoint = h.face.get(
        "midpoint", self.geometry.center_of_mass(np.stack([v["pos"] for v in h.face.vertex_iter()]))
    )
    return np.array([h.dest["pos"], midpoint, h.orig["pos"]])

generate_graph_and_corners

generate_graph_and_corners(
    tri: ndarray, h: HalfEdge | None = None
) -> tuple[HalfEdgeGraph, tuple[Vertex, Vertex, Vertex]]

Return a copy of the domain with positions mapped from barycentric to Euclidean coordinates.

Source code in pleat/conway/operators.py
def generate_graph_and_corners(
    self, tri: np.ndarray, h: HalfEdge | None = None
) -> tuple[HalfEdgeGraph, tuple[Vertex, Vertex, Vertex]]:
    """Return a copy of the domain with positions mapped from barycentric to Euclidean coordinates."""
    result, corners = super(GeometricConwayOperator, self).generate_graph_and_corners(tri, h)

    to_euclidean = self.geometry.barycentric_to_euclidean_map(tri)
    # this could be vectorized
    for v in result.vertices:
        v["pos"] = to_euclidean(v["pos"])
    return result, corners

__call__

__call__(
    graph: GeometricHEG,
    recompute_lengths_and_angles: bool = True,
    **kwargs: object
) -> GeometricHEG

Apply the geometric operator to graph.

Parameters:

Name Type Description Default
graph GeometricHEG

Geometric half-edge graph to operate on.

required
recompute_lengths_and_angles bool

If True, recompute edge lengths and interior angles after substitution.

True
**kwargs object

Forwarded to :meth:TopologicalConwayOperator.__call__.

{}

Returns:

Type Description
GeometricHEG

The transformed graph.

Source code in pleat/conway/operators.py
def __call__(
    self,
    graph: GeometricHEG,
    recompute_lengths_and_angles: bool = True,
    **kwargs: object,
) -> GeometricHEG:
    """Apply the geometric operator to ``graph``.

    Args:
        graph: Geometric half-edge graph to operate on.
        recompute_lengths_and_angles: If True, recompute edge lengths and
            interior angles after substitution.
        **kwargs: Forwarded to :meth:`TopologicalConwayOperator.__call__`.

    Returns:
        The transformed graph.
    """
    assert isinstance(graph, GeometricHEG)
    self.geometry = graph.geometry
    result = super().__call__(graph, **kwargs)
    if recompute_lengths_and_angles:
        result.recompute_lengths_and_angles()
    return result

get_fundamental_domain_graph_to_render

get_fundamental_domain_graph_to_render(
    delete_color: tuple[float, float, float] = (
        0.85,
        0.15,
        0.15,
    ),
    join_color: tuple[float, float, float] = (
        0.15,
        0.65,
        0.2,
    ),
    keep_color: tuple[float, float, float] = (
        0.3,
        0.3,
        0.3,
    ),
) -> HalfEdgeGraph

Build a copy of the fundamental-domain graph, colouring elements by their role.

Vertices and edges flagged as delete are coloured delete_color (default: red), and edges become dashed via the existing renderer behaviour; vertices flagged as join are coloured join_color (default: green); everything else uses keep_color.

Parameters:

Name Type Description Default
delete_color tuple[float, float, float]

RGB triple for delete-marked elements.

(0.85, 0.15, 0.15)
join_color tuple[float, float, float]

RGB triple for join-marked vertices.

(0.15, 0.65, 0.2)
keep_color tuple[float, float, float]

RGB triple for retained elements.

(0.3, 0.3, 0.3)

Returns:

Type Description
HalfEdgeGraph

A Euclidean-projected copy of the graph with color_key attributes set, ready to render.

Source code in pleat/conway/operators.py
def get_fundamental_domain_graph_to_render(
    self,
    delete_color: tuple[float, float, float] = (0.85, 0.15, 0.15),
    join_color: tuple[float, float, float] = (0.15, 0.65, 0.20),
    keep_color: tuple[float, float, float] = (0.30, 0.30, 0.30),
) -> HalfEdgeGraph:
    """Build a copy of the fundamental-domain graph, colouring elements by their role.

    Vertices and edges flagged as ``delete`` are coloured *delete_color*
    (default: red), and edges become dashed via the existing renderer behaviour;
    vertices flagged as ``join`` are coloured *join_color* (default: green); everything else uses *keep_color*.

    Args:
        delete_color: RGB triple for delete-marked elements.
        join_color: RGB triple for join-marked vertices.
        keep_color: RGB triple for retained elements.

    Returns:
        A Euclidean-projected copy of the graph with ``color_key`` attributes set, ready to render.
    """
    from ..half import EuclideanGeometry

    # Project barycentric vertex positions back to a canonical Euclidean triangle.
    graph_copy, (_, _, _) = self.graph.copy(deepcopy_attributes=False, return_mappings=True)
    graph_copy.geometry = EuclideanGeometry
    to_euclidean = EuclideanGeometry.barycentric_to_euclidean_map(self._SHOW_REFERENCE_TRIANGLE)
    for v in graph_copy.vertices:
        v["pos"] = to_euclidean(v["pos"])

    # Assign colours based on role.
    for v in graph_copy.vertices:
        if v.attributes.get("delete", False):
            v["color_key"] = delete_color
        elif v.attributes.get("join", False):
            v["color_key"] = join_color
        else:
            v["color_key"] = keep_color
    for h in graph_copy.halfedges:
        if h.attributes.get("delete", False) or h.rev.attributes.get("delete", False):
            h["color_key"] = delete_color
        elif "color_key" not in h.attributes:
            h["color_key"] = keep_color
    return graph_copy

render

render(**show_kwargs: object) -> Rendering

Render the fundamental domain graph with styling from :meth:get_fundamental_domain_graph_to_render.

Source code in pleat/conway/operators.py
def render(self, **show_kwargs: object) -> Rendering:
    """Render the fundamental domain graph with styling from :meth:`get_fundamental_domain_graph_to_render`."""

    fundamental_domain_graph = self.get_fundamental_domain_graph_to_render()
    kwargs = dict(
        line_width="50%",
        face_inset=0,
    )
    kwargs.update(show_kwargs)
    return fundamental_domain_graph.render(**kwargs)

show

show(**kwargs: object) -> None

Render the fundamental domain graph with styling from :meth:get_fundamental_domain_graph_to_render and display it.

Source code in pleat/conway/operators.py
def show(self, **kwargs: object) -> None:
    """Render the fundamental domain graph with styling from :meth:`get_fundamental_domain_graph_to_render` and display it."""
    rendering = self.render(**kwargs)
    rendering.show()

TopologicalConwayOperator

TopologicalConwayOperator(
    graph: HalfEdgeGraph, v1: Vertex, vf: Vertex, v2: Vertex
)

Apply a Conway operator to a half-edge graph by substituting a fundamental domain into each face triangle.

The fundamental domain is a small half-edge graph with three marked vertices (v1, vf, v2) corresponding to a vertex, face center, and adjacent vertex of each triangle.

Store the fundamental domain and its three corner vertices.

Parameters:

Name Type Description Default
graph HalfEdgeGraph

Half-edge graph encoding the fundamental domain.

required
v1 Vertex

Corner mapped to h.dest of each triangle.

required
vf Vertex

Corner mapped to the face midpoint of each triangle.

required
v2 Vertex

Corner mapped to h.orig of each triangle.

required
Source code in pleat/conway/operators.py
def __init__(self, graph: HalfEdgeGraph, v1: Vertex, vf: Vertex, v2: Vertex) -> None:
    """Store the fundamental domain and its three corner vertices.

    Args:
        graph: Half-edge graph encoding the fundamental domain.
        v1: Corner mapped to ``h.dest`` of each triangle.
        vf: Corner mapped to the face midpoint of each triangle.
        v2: Corner mapped to ``h.orig`` of each triangle.
    """
    self.graph = graph
    self.v1 = v1
    self.vf = vf
    self.v2 = v2
    assert all(v in graph.vertices for v in (v1, vf, v2))

show

show() -> None

Render the fundamental domain graph for visualization.

Source code in pleat/conway/operators.py
def show(self) -> None:
    """Render the fundamental domain graph for visualization."""
    self.graph.show(scale=300, line_width=0.03, render_faces=False)

get_tri

get_tri(h: HalfEdge) -> 'np.ndarray | None'

Return the triangle for half-edge h, or None for purely topological operators.

Source code in pleat/conway/operators.py
def get_tri(self, h: HalfEdge) -> "np.ndarray | None":
    """Return the triangle for half-edge ``h``, or None for purely topological operators."""
    return None

generate_graph_and_corners

generate_graph_and_corners(
    tri: "np.ndarray | None", h: HalfEdge | None = None
) -> tuple[HalfEdgeGraph, tuple[Vertex, Vertex, Vertex]]

Return a copy of the fundamental-domain graph and its three corner vertices.

Parameters:

Name Type Description Default
tri 'np.ndarray | None'

Optional reference triangle for geometric subclasses.

required
h HalfEdge | None

Optional original half-edge; if given, every interior vertex of the copy gets pre_conway = h for traceability.

None

Returns:

Type Description
tuple[HalfEdgeGraph, tuple[Vertex, Vertex, Vertex]]

Tuple (graph_copy, (v1, vf, v2)).

Source code in pleat/conway/operators.py
def generate_graph_and_corners(
    self, tri: "np.ndarray | None", h: HalfEdge | None = None
) -> tuple[HalfEdgeGraph, tuple[Vertex, Vertex, Vertex]]:
    """Return a copy of the fundamental-domain graph and its three corner vertices.

    Args:
        tri: Optional reference triangle for geometric subclasses.
        h: Optional original half-edge; if given, every interior vertex of
            the copy gets ``pre_conway = h`` for traceability.

    Returns:
        Tuple ``(graph_copy, (v1, vf, v2))``.
    """
    graph, (v_map, _, _) = self.graph.copy(deepcopy_attributes=False, return_mappings=True)
    if h is not None:
        for v in graph.vertices:
            if not v.on_border():  # todo: also assign 'pre_conway' to border vertices, as a set
                v["pre_conway"] = h
    return graph, (v_map[self.v1], v_map[self.vf], v_map[self.v2])

__call__

__call__(
    graph: HalfEdgeGraph,
    *,
    faces: "set[Face] | Callable[[Face], bool] | None" = None,
    delete_on_border: bool = True,
    delete_inner_border: bool = False,
    inplace: bool = False
) -> HalfEdgeGraph

Apply the operator to graph, optionally restricted to faces.

Parameters:

Name Type Description Default
graph HalfEdgeGraph

Input half-edge graph; left untouched unless inplace=True.

required
faces 'set[Face] | Callable[[Face], bool] | None'

Faces to apply the operator to. Either an explicit set of faces, or a callable face -> bool used to filter graph.faces. Defaults to all faces.

None
delete_on_border bool

If True, delete faces whose original border edge was marked for deletion.

True
delete_inner_border bool

If True, also clear the delete flag on inner border edges.

False
inplace bool

If True, mutate graph directly. If False (default), operate on a deep-ish copy and preserve pre_conway references back to the original objects.

False

Returns:

Type Description
HalfEdgeGraph

The transformed graph (the input itself if inplace=True, otherwise a copy).

Source code in pleat/conway/operators.py
def __call__(
    self,
    graph: HalfEdgeGraph,
    *,
    faces: "set[Face] | Callable[[Face], bool] | None" = None,
    delete_on_border: bool = True,
    delete_inner_border: bool = False,
    inplace: bool = False,
) -> HalfEdgeGraph:
    """Apply the operator to ``graph``, optionally restricted to ``faces``.

    Args:
        graph: Input half-edge graph; left untouched unless ``inplace=True``.
        faces: Faces to apply the operator to. Either an explicit set of
            faces, or a callable ``face -> bool`` used to filter
            ``graph.faces``. Defaults to all faces.
        delete_on_border: If True, delete faces whose original border edge
            was marked for deletion.
        delete_inner_border: If True, also clear the ``delete`` flag on
            inner border edges.
        inplace: If True, mutate ``graph`` directly. If False (default),
            operate on a deep-ish copy and preserve ``pre_conway`` references
            back to the original objects.

    Returns:
        The transformed graph (the input itself if ``inplace=True``, otherwise a copy).
    """
    if not inplace:
        graph, (v_map, h_map, f_map) = graph.copy(return_mappings=True)
        # An explicit ``faces`` collection still references the input graph; translate
        # those Face objects into the copy. (A callable filter is applied later, after
        # ``graph`` already points at the copy, so it needs no remapping.)
        if faces is not None and not callable(faces):
            faces = {f_map[f] for f in faces}
        v_map, h_map, f_map = [invert_mapping(m) for m in (v_map, h_map, f_map)]
        obj_map = dict()
        obj_map.update(v_map)
        obj_map.update(h_map)
        obj_map.update(f_map)
        for obj in obj_map.keys():
            if obj is not None and "pre_conway" in obj:
                del obj["pre_conway"]

    # apply the operator to a set of halfedges in a graph
    assert isinstance(graph, HalfEdgeGraph)
    if faces is None:
        faces = graph.faces
    elif callable(faces):
        faces = {f for f in graph.faces if faces(f)}
    halfedges = [h for f in faces for h in f.halfedge_iter()]
    assert all(isinstance(h, HalfEdge) for h in halfedges)
    affected_faces = {h.face for h in halfedges}
    assert None not in affected_faces, "Cannot apply Conway operator to boundary edge"  # Or can we?
    old_halfedges = frozenset(graph.halfedges)
    v1_out_lookup = dict()
    v2_out_lookup = dict()
    vf_lookup = dict()
    vf_set = set()

    graphs_and_corners = [self.generate_graph_and_corners(self.get_tri(h), h) for h in halfedges]

    for gc, h in zip(graphs_and_corners, halfedges):
        orig_face = h.face
        con_graph, (v1, vf, v2) = gc

        # add reference to old face/vertex to new face/vertex
        for new_vertex, old_obj in [(v1, h.dest), (vf, h.face), (v2, h.orig)]:
            if new_vertex.attributes.get("delete", False):
                new_vertex.get_outgoing_border().rev.face["pre_conway"] = old_obj
            else:
                new_vertex["pre_conway"] = old_obj

        # glue v1, v2 to h.dest, h.orig
        graph.add_graph(con_graph)
        v1_out = v1.get_outgoing_border()
        v2_out = v2.get_outgoing_border()
        graph.glue_v2v(v1_out=h.nex, v2_out=v1_out)
        graph.glue_v2v(v1_out=h, v2_out=v2_out)
        inbetween_face = Face(any_side=h)
        h.face = inbetween_face
        k = v2_out
        orig_face.any_side = k
        while k.orig is not h.dest:
            k.face = orig_face
            k = k.nex
        k = v1_out
        while k.orig is not h.orig:
            k.face = inbetween_face
            k = k.nex
        graph.add_face(inbetween_face)

        v1_out_lookup[h] = v1_out
        v2_out_lookup[h] = v2_out
        vf_lookup[h] = vf
        vf_set.add(vf)

    # handle the insides of faces
    for h in halfedges:
        v2_out = v2_out_lookup[h]
        if v2_out.pre in old_halfedges:
            # v2_out.pre was not in the set of halfedges that the operator was applied to
            raise NotImplementedError
        current = v2_out
        while current.dest not in vf_set:
            next = current.nex
            HalfEdgeGraph.glue_e2e(graph, current, current.pre)
            current = next
        HalfEdgeGraph.glue_e2e(graph, current, current.pre)

    # handle the original edges
    to_process = copy(halfedges)
    while to_process:
        h = to_process.pop()
        if h.rev in halfedges:
            # delete old edge, then zip
            to_process.remove(h.rev)
            HalfEdgeGraph.delete_edge(graph, h)
            current = v1_out_lookup[h]
            f = current.face
            while f in graph.faces:
                next = current.nex
                HalfEdgeGraph.glue_e2e(graph, current, current.pre)
                current = next
        else:
            if True:  # not delete_on_border or not h.rev.on_border(): #Fixme
                for k in h.face.halfedge_iter():
                    if (not delete_inner_border) or h.rev.on_border():
                        k["delete"] = False
                        k.rev["delete"] = False
                    k.rev["border_delete"] = True

            if h.rev.on_border():
                graph.delete_face(h.face)
            else:
                HalfEdgeGraph.delete_edge(graph, h)
            # TODO: if on border, delete. else, do not delete adjacent. for now: delete in neither case

    # TODO: keep a record of which edges to delete in the end..
    # and a record of all affected vertices to update angles

    to_delete = set()
    to_process = copy(graph.halfedges)  # this is bad for performance: make everything work locally!
    to_keep = set()
    while to_process:
        h = to_process.pop()
        to_process.remove(h.rev)
        if h.attributes.get("delete", False):
            # only delete edges if their reverse also wants to be deleted
            if h.rev.attributes.get("delete", False):
                to_delete.update({h, h.rev})
                continue
            else:
                pass
                # del h.attributes['delete']
        to_keep.update({h, h.rev})

    assert not to_keep.intersection(to_delete)
    assert (
        to_keep.union(to_delete) == graph.halfedges
    ), f"{graph.halfedges.difference(to_keep.union(to_delete))}, {to_keep.union(to_delete).difference(graph.halfedges)}"

    faces_to_keep = set()
    faces_to_maybe_remove = set()
    while to_keep:
        # find the new faces
        h = to_keep.pop()
        initial = h
        f = h.face  # what about border?
        if f is not None:
            f.any_side = h
            faces_to_keep.add(f)
        while True:  # walk around the new face until cycle closes
            nex = h.nex
            while nex in to_delete:
                nex = nex.rev.nex
            h.nex = nex
            nex.pre = h
            h.dest.any_outgoing = nex
            if nex is initial:
                break
            else:
                faces_to_maybe_remove.add(nex.face)
                nex.face = f
                to_keep.remove(nex)
            h = nex

    graph.halfedges.difference_update(to_delete)
    graph.faces.difference_update(faces_to_maybe_remove.difference(faces_to_keep))

    graph.vertices.difference_update([v for v in (h.orig for h in to_delete) if v.any_outgoing in to_delete])

    if delete_on_border:
        for e in list(graph.border_edges()):
            if e in graph.halfedges:
                if e.rev.attributes.get("border_delete", False):
                    graph.delete_face(e.rev.face)
        # delete dangling faces
        while True:
            deleted_any = False
            for f in list(graph.faces):
                if not any(f.face_iter()):
                    graph.delete_face(f)
                    deleted_any = True
            if not deleted_any:
                break
    for e in graph.halfedges:
        if "border_delete" in e.attributes:
            del e["border_delete"]

    to_join = {v for v in graph.vertices if v.attributes.get("join", False)}
    for v in to_join:
        if v.order() == 2:  # TODO: check this beforehand
            HalfEdgeGraph.join_vertex(graph, v)

    if not inplace:
        for objs in (graph.vertices, graph.halfedges, graph.faces):
            for obj in objs:
                if "pre_conway" in obj.attributes:
                    obj["pre_conway"] = obj_map[obj["pre_conway"]]
    return graph

alternating_flagstone_graph

alternating_flagstone_graph(
    t: float = 1 / 3,
) -> GeometricConwayOperator

Construct the alternating flagstone operator with parameter t.

Source code in pleat/conway/factories.py
def alternating_flagstone_graph(t: float = 1 / 3) -> GeometricConwayOperator:
    """Construct the alternating flagstone operator with parameter t."""
    v1 = (0, -1)
    vf = (1, 0)
    v2 = (0, 1)
    v0 = (0, 0)
    v1f = (t, -1 + t)
    v2f = (t, 1 - t)
    G = nx.Graph()
    nx.add_cycle(G, [v1, v1f, vf, v2f, v2, v0], delete=True)
    del G.edges[v1, v1f]["delete"]
    del G.edges[v2, v2f]["delete"]
    G.add_edge(v2f, v1, color_key=(1, 0, 0))
    G.add_nodes_from([v0], join=True)
    G.add_nodes_from([vf], delete=True)
    G.add_edges_from([[v0, v2f], [v2f, v1f]])

    # construct EHEG and ConwayOperator
    heg, v_lookup = EHEG_from_nx(G, return_v_lookup=True)
    return GeometricConwayOperator(heg, *(v_lookup[v] for v in [v1, vf, v2]))

ambo_graph

ambo_graph() -> GeometricConwayOperator

Construct the Conway ambo (rectification) operator.

Source code in pleat/conway/factories.py
def ambo_graph() -> GeometricConwayOperator:
    """Construct the Conway ambo (rectification) operator."""
    # define vertex positions
    v1 = (0, -1)
    vf = (1, 0)
    v2 = (0, 1)
    v12 = (0, 0)
    v1f = (1 / 2, -1 / 2)
    v2f = (1 / 2, 1 / 2)
    # construct nx graph with delete and join attributes
    G = nx.Graph()
    nx.add_cycle(G, [v1, v1f, vf, v2f, v2, v12], delete=True)
    G.add_nodes_from([v1, vf, v2], delete=True)
    G.add_nodes_from([v1f, v2f], join=True)
    G.add_edges_from([[v12, v1f], [v12, v2f]])

    # construct EHEG and ConwayOperator
    heg, v_lookup = EHEG_from_nx(G, return_v_lookup=True)
    return GeometricConwayOperator(heg, *(v_lookup[v] for v in [v1, vf, v2]))

chamfer_graph

chamfer_graph(t: float = 1 / 2) -> GeometricConwayOperator

Construct the Conway chamfer operator, derived from loft with the v1-v2 edge deleted.

Source code in pleat/conway/factories.py
def chamfer_graph(t: float = 1 / 2) -> GeometricConwayOperator:
    """Construct the Conway chamfer operator, derived from loft with the v1-v2 edge deleted."""
    assert t < 1
    result = loft_graph(t)
    e = [e for e in result.v1.outgoing_iter() if e.dest is result.v2][0]
    e.rev.attributes["delete"] = True
    e.attributes["delete"] = True
    return result

dual_graph

dual_graph() -> GeometricConwayOperator

Construct the Conway dual operator.

Source code in pleat/conway/factories.py
def dual_graph() -> GeometricConwayOperator:
    """Construct the Conway dual operator."""
    v1 = (0, -1)
    vf = (1, 0)
    v2 = (0, 1)
    ve = (0, 0)
    G = nx.Graph()
    nx.add_cycle(G, [v1, vf, v2, ve], delete=True)
    G.add_nodes_from([v1, v2], delete=True)
    G.add_nodes_from([ve], join=True)
    G.add_edge(ve, vf)

    # construct EHEG and ConwayOperator
    heg, v_lookup = EHEG_from_nx(G, return_v_lookup=True)
    return GeometricConwayOperator(heg, *(v_lookup[v] for v in [v1, vf, v2]))

expand_graph

expand_graph(t: float = 1 / 2) -> GeometricConwayOperator

Construct the Conway expand operator with offset parameter t (must be < 1).

Source code in pleat/conway/factories.py
def expand_graph(t: float = 1 / 2) -> GeometricConwayOperator:
    """Construct the Conway expand operator with offset parameter t (must be < 1)."""
    assert t < 1
    v1 = (0, -1)
    vf = (1, 0)
    v2 = (0, 1)
    v12 = (0, -1 + t)
    v21 = (0, 1 - t)
    v1f = (t, -1 + t)
    v2f = (t, 1 - t)

    G = nx.Graph()
    nx.add_cycle(G, [v2, v21, v12, v1, v1f, vf, v2f], delete=True)

    G.add_edges_from([[v1f, v12], [v2f, v21], [v1f, v2f]])
    G.add_nodes_from([v12, v21, v1f, v2f], join=True)
    G.add_nodes_from([vf], delete=True)

    # construct EHEG and ConwayOperator
    heg, v_lookup = EHEG_from_nx(G, return_v_lookup=True)
    return GeometricConwayOperator(heg, *(v_lookup[v] for v in [v1, vf, v2]))

flagstone_pvitelli_graph

flagstone_pvitelli_graph(
    t: float = 1 / 4,
) -> GeometricConwayOperator

Construct the Pvitelli flagstone operator with parameter t (must be < 1).

Source code in pleat/conway/factories.py
def flagstone_pvitelli_graph(t: float = 1 / 4) -> GeometricConwayOperator:
    """Construct the Pvitelli flagstone operator with parameter t (must be < 1)."""
    assert t < 1
    v1 = (0, -1)
    vf = (1, 0)
    v2 = (0, 1)
    vL1 = (0, -1 + t)
    vL2 = (0, 1 - t)
    vL1f = (t / 2, -1 + t / 2)
    vL2f = (t / 2, 1 - t / 2)
    vV1 = (t, -1 + t)
    vV2 = (t, 1 - t)
    vN1 = (t / 2, -1 + 3 / 2 * t)
    vN2 = (t / 2, 1 - 3 / 2 * t)
    vN1e = (0, -1 + 3 / 2 * t)
    vN2e = (0, 1 - 3 / 2 * t)

    G = nx.Graph()
    nx.add_cycle(G, [v2, vL2, vN2e, vN1e, vL1, v1, vL1f, vV1, vf, vV2, vL2f], delete=True)
    nx.add_cycle(G, [vL2, vN2, vN1, vL1, vV1, vV2])
    G.add_edges_from([[vL1f, vL1], [vL2f, vL2], [vN1e, vN1], [vN2e, vN2], [vN1, vV1], [vN2, vV2]])
    G.add_nodes_from([vL1f, vL2f, vN1e, vN2e], join=True)

    # label the points which will be joined that are closer to v1
    G.add_nodes_from([vL1], label="L")
    G.add_nodes_from([vV1], label="V")
    # label both copies of the inner node
    G.add_nodes_from([vN1], label="N1")
    G.add_nodes_from([vN2], label="N2")

    G.add_nodes_from([v1, v2, vf], delete=True)

    # construct EHEG and ConwayOperator
    heg, v_lookup = EHEG_from_nx(G, return_v_lookup=True)

    v_lookup[vf].get_outgoing_border().rev.face["is_central_polygon"] = True

    return GeometricConwayOperator(heg, *(v_lookup[v] for v in [v1, vf, v2]))

goldberg2_graph

goldberg2_graph() -> GeometricConwayOperator

Construct the Goldberg-2 subdivision operator.

Source code in pleat/conway/factories.py
def goldberg2_graph() -> GeometricConwayOperator:
    """Construct the Goldberg-2 subdivision operator."""
    # define vertex positions
    v1 = (0, -1)
    vf = (1, 0)
    v2 = (0, 1)
    v12 = (0, 0)
    v1f = (1 / 2, -1 / 2)
    v2f = (1 / 2, 1 / 2)
    # construct nx graph with delete and join attributes
    G = nx.Graph()
    nx.add_cycle(G, [v1, v1f, vf, v2f, v2, v12], delete=True)
    del G.edges[v1, v12]["delete"]
    del G.edges[v12, v2]["delete"]
    G.add_nodes_from([vf], delete=True)
    G.add_nodes_from([v1f, v2f], join=True)
    G.add_edges_from([[v12, v1f], [v12, v2f]])

    # construct EHEG and ConwayOperator
    heg, v_lookup = EHEG_from_nx(G, return_v_lookup=True)
    return GeometricConwayOperator(heg, *(v_lookup[v] for v in [v1, vf, v2]))

gyro_graph

gyro_graph(
    g: tuple[float, float] = (1 / 4, -1 / 4)
) -> GeometricConwayOperator

Construct the Conway gyro operator with snub point position g.

Source code in pleat/conway/factories.py
def gyro_graph(g: tuple[float, float] = (1 / 4, -1 / 4)) -> GeometricConwayOperator:
    """Construct the Conway gyro operator with snub point position g."""
    v1 = (0, -1)
    vf = (1, 0)
    v2 = (0, 1)
    ve = (0, 0)
    G = nx.Graph()
    nx.add_cycle(G, [v1, vf, v2, ve], delete=True)
    G.add_edges_from([[g, ve], [g, v1], [g, vf]])
    G.add_node(ve, join=True)

    # construct EHEG and ConwayOperator
    heg, v_lookup = EHEG_from_nx(G, return_v_lookup=True)
    return GeometricConwayOperator(heg, *(v_lookup[v] for v in [v1, vf, v2]))

join_graph

join_graph() -> GeometricConwayOperator

Construct the Conway join operator.

Source code in pleat/conway/factories.py
def join_graph() -> GeometricConwayOperator:
    """Construct the Conway join operator."""
    # define vertex positions
    v1 = (0, -1)
    vf = (1, 0)
    v2 = (0, 1)
    # construct nx graph with delete and join attributes
    G = nx.Graph()
    nx.add_cycle(G, [v1, vf, v2])
    G.add_edge(v2, v1, delete=True)
    # construct EHEG and ConwayOperator
    heg, v_lookup = EHEG_from_nx(G, return_v_lookup=True)
    return GeometricConwayOperator(heg, *(v_lookup[v] for v in [v1, vf, v2]))

kis_graph

kis_graph() -> GeometricConwayOperator

Construct the Conway kis (raising) operator.

Source code in pleat/conway/factories.py
def kis_graph() -> GeometricConwayOperator:
    """Construct the Conway kis (raising) operator."""
    # define vertex positions
    v1 = (0, -1)
    vf = (1, 0)
    v2 = (0, 1)
    # construct nx graph with delete and join attributes
    G = nx.Graph()
    nx.add_cycle(G, [v1, vf, v2])

    # construct EHEG and ConwayOperator
    heg, v_lookup = EHEG_from_nx(G, return_v_lookup=True)
    return GeometricConwayOperator(heg, *(v_lookup[v] for v in [v1, vf, v2]))

lace_graph

lace_graph(
    t: float = 1 / 2, join: bool = False
) -> GeometricConwayOperator

Construct the lace operator with offset t. If join is True, merge the v1-v2 edge.

Source code in pleat/conway/factories.py
def lace_graph(t: float = 1 / 2, join: bool = False) -> GeometricConwayOperator:
    """Construct the lace operator with offset t. If join is True, merge the v1-v2 edge."""
    assert t < 1
    v1 = (0, -1)
    vf = (1, 0)
    v2 = (0, 1)
    vc = (t, 0)
    v1f = (t, -1 + t)
    v2f = (t, 1 - t)

    G = nx.Graph()
    nx.add_cycle(G, [v1, v1f, vf, v2f, v2], delete=True)
    if not join:
        del G.edges[v1, v2]["delete"]

    G.add_edges_from([[vc, v1], [vc, v2], [vc, v1f], [vc, v2f]])
    G.add_nodes_from([v1f, v2f], join=True)
    G.add_nodes_from([vf], delete=True)

    # construct EHEG and ConwayOperator
    heg, v_lookup = EHEG_from_nx(G, return_v_lookup=True)
    return GeometricConwayOperator(heg, *(v_lookup[v] for v in [v1, vf, v2]))

loft_graph

loft_graph(t: float = 1 / 2) -> GeometricConwayOperator

Construct the loft operator with edge offset parameter t (must be < 1).

Source code in pleat/conway/factories.py
def loft_graph(t: float = 1 / 2) -> GeometricConwayOperator:
    """Construct the loft operator with edge offset parameter t (must be < 1)."""
    assert t < 1
    v1 = (0, -1)
    vf = (1, 0)
    v2 = (0, 1)
    v1ft = (t, -1 + t)
    v2ft = (t, 1 - t)
    G = nx.Graph()
    nx.add_cycle(G, [v1, v1ft, v2ft, v2])
    G.add_nodes_from([vf], delete=True)
    G.add_edges_from([[v1ft, vf], [vf, v2ft]], delete=True)

    # construct EHEG and ConwayOperator
    heg, v_lookup = EHEG_from_nx(G, return_v_lookup=True)
    return GeometricConwayOperator(heg, *(v_lookup[v] for v in [v1, vf, v2]))

meta_graph

meta_graph() -> GeometricConwayOperator

Construct the Conway meta operator, which includes all edges from dual and kis.

Source code in pleat/conway/factories.py
def meta_graph() -> GeometricConwayOperator:
    """Construct the Conway meta operator, which includes all edges from dual and kis."""
    v1 = (0, -1)
    vf = (1, 0)
    v2 = (0, 1)
    ve = (0, 0)
    G = nx.Graph()
    nx.add_cycle(G, [v1, vf, v2, ve])
    G.add_nodes_from([v1, v2])
    G.add_nodes_from([ve])
    G.add_edge(ve, vf)

    # construct EHEG and ConwayOperator
    heg, v_lookup = EHEG_from_nx(G, return_v_lookup=True)
    return GeometricConwayOperator(heg, *(v_lookup[v] for v in [v1, vf, v2]))

ortho_graph

ortho_graph() -> GeometricConwayOperator

Construct the Conway ortho operator, which combines the orignal graph and its dual

Source code in pleat/conway/factories.py
def ortho_graph() -> GeometricConwayOperator:
    """Construct the Conway ortho operator, which combines the orignal graph and its dual"""
    v1 = (0, -1)
    vf = (1, 0)
    v2 = (0, 1)
    ve = (0, 0)
    G = nx.Graph()
    nx.add_cycle(G, [v1, vf, v2, ve])
    G.add_edge(ve, vf)
    G.edges[v1, vf]["delete"] = True
    G.edges[v2, vf]["delete"] = True

    # construct EHEG and ConwayOperator
    heg, v_lookup = EHEG_from_nx(G, return_v_lookup=True)
    return GeometricConwayOperator(heg, *(v_lookup[v] for v in [v1, vf, v2]))

shrink_rotate_graph

shrink_rotate_graph(
    t: float = 1 / 2,
) -> GeometricConwayOperator

Construct the shrink-rotate Conway operator with parameter t.

The operator creates new faces for each original vertex, shrinks the original faces and creates a ring of quadrilateral faces between the twists; the central polygon is later rotated and scaled around the reciprocal-figure center to produce a flat-foldable crease pattern (see :mod:pleat.shrink_rotate).

Each twist face is marked with the 'shrink_rotate' attribute so downstream code can identify it.

Source code in pleat/conway/factories.py
def shrink_rotate_graph(t: float = 1 / 2) -> GeometricConwayOperator:
    """Construct the shrink-rotate Conway operator with parameter *t*.

    The operator creates new faces for each original vertex, shrinks the original faces and creates a ring of quadrilateral faces between the twists; the central polygon is later
    rotated and scaled around the reciprocal-figure center to produce a
    flat-foldable crease pattern (see :mod:`pleat.shrink_rotate`).

    Each twist face is marked with the ``'shrink_rotate'`` attribute so
    downstream code can identify it.
    """
    v1 = (0, -1)
    vf = (1, 0)
    v2 = (0, 1)
    v12t = (0, -1 + t)
    v1ft = (t, -1 + t)
    v21t = (0, 1 - t)
    v2ft = (t, 1 - t)
    G = nx.Graph()
    nx.add_cycle(G, [v1, v1ft, vf, v2ft, v2, v21t, v12t], delete=True)
    G.add_nodes_from([v1, vf, v2], delete=True)
    G.add_nodes_from([v12t, v21t], join=True)
    G.add_edges_from([[v12t, v1ft], [v21t, v2ft], [v1ft, v2ft]])

    # construct EHEG and ConwayOperator
    heg, v_lookup = EHEG_from_nx(G, return_v_lookup=True)
    v_lookup[vf].get_outgoing_border().rev.face["shrink_rotate"] = True
    return GeometricConwayOperator(heg, *(v_lookup[v] for v in [v1, vf, v2]))

starify_graph

starify_graph(t: float = 1 / 3) -> GeometricConwayOperator

Construct the starify operator with parameter t controlling star point depth.

Source code in pleat/conway/factories.py
def starify_graph(t: float = 1 / 3) -> GeometricConwayOperator:
    """Construct the starify operator with parameter t controlling star point depth."""
    v1 = (0, -1)
    vf = (1, 0)
    v2 = (0, 1)
    v0 = (0, 0)
    v1f = (t, -1 + t)
    v2f = (t, 1 - t)
    G = nx.Graph()
    nx.add_cycle(G, [v1, v1f, vf, v2f, v2, v0], delete="True")
    G.add_nodes_from([v0], join=True)
    G.add_edges_from([[v0, v2f], [v2f, v1f]])

    # construct EHEG and ConwayOperator
    heg, v_lookup = EHEG_from_nx(G, return_v_lookup=True)
    return GeometricConwayOperator(heg, *(v_lookup[v] for v in [v1, vf, v2]))

truncate_graph

truncate_graph(t: float = 1 / 2) -> GeometricConwayOperator

Construct the Conway truncate operator with cut depth t.

Source code in pleat/conway/factories.py
def truncate_graph(t: float = 1 / 2) -> GeometricConwayOperator:
    """Construct the Conway truncate operator with cut depth t."""
    v1 = (0, -1)
    vf = (1, 0)
    v2 = (0, 1)
    v12t = (0, -1 + t)
    v1ft = (t / 2, -1 + t / 2)
    v21t = (0, 1 - t)
    v2ft = (t / 2, 1 - t / 2)
    G = nx.Graph()
    nx.add_cycle(G, [v1, v1ft, vf, v2ft, v2, v21t, v12t], delete=True)
    del G.edges[v12t, v21t]["delete"]
    G.add_nodes_from([v1, vf, v2], delete=True)
    G.add_nodes_from([v1ft, v2ft], join=True)
    G.add_edges_from([[v12t, v1ft], [v21t, v2ft]])

    # construct EHEG and ConwayOperator
    heg, v_lookup = EHEG_from_nx(G, return_v_lookup=True)
    return GeometricConwayOperator(heg, *(v_lookup[v] for v in [v1, vf, v2]))