Conway Operators¶
Conway Operators can be used to transform one tiling into another one. They are best known for constructing polyhedra (see here for a very nice demo), but can be equally applied to planar tilings.
pleat implements numerous such operators, and allows for straightforward construction of additional ones. This notebook demonstrates how to use them with a number of examples.
import matplotlib
matplotlib.rcParams['figure.figsize'] = (5, 5)
from pleat import (
conway,
example_graphs,
example_tilesets,
)
from pleat.rendering import multi_show
Example 1: Dual of 4.6.12¶
The dual of a tiling has a vertex for every face in the original tiling, and a face for every (interior) vertex in the original tiling.
G = example_graphs.from_tiles(example_tilesets.t_4_6_12(), rings=2)
G_dual = conway.dual_graph()(G) # by default the original G is left untouched; pass inplace=True to mutate
multi_show([G, G_dual], titles=['original', 'dual'], render_vertices=False)
Shorthand: methods on the graph itself¶
Every operator factory in pleat.conway is also available as a method on the graph, so
the call above can be written more concisely as G.dual(). The methods accept both the
factory's parameters and the standard call kwargs (inplace, delete_on_border, faces, …),
and they're chainable: G.ambo().dual().truncate(0.4).dual().
The rest of this notebook uses the shorthand form.
Example 2: kis & ambo, deleting or keeping border faces¶
By default, potentially incomplete faces on the boundary of the original graph are deleted. This is often useful, but you can disable it by setting delete_on_border=False.
G = example_graphs.from_tiles(example_tilesets.t_3_3_4_3_4(), rings=2, base_tile=1)
G_kis = G.kis()
G_kis_with_border = G.kis(delete_on_border=False)
G_ambo = G.ambo()
G_ambo_with_border = G.ambo(delete_on_border=False)
multi_show(
[G, G_kis, G_kis_with_border, G, G_ambo, G_ambo_with_border],
titles=['original', 'kis', 'kis with border', 'original', 'ambo', 'ambo with border'],
render_vertices=False, ncols=3
)
Example 3: Operators with parameters¶
Some operators have parameters, which influence the final geometry (not the topology).
The truncate operator has a single parameter, influencing how large the new faces are.
The gyro operator has two parameters, which determine where inside the fundamental domain the new vertex is placed.
G = example_graphs.from_tiles(example_tilesets.platonic(6), rings=2)
truncate_graphs = {
truncate_depth: G.truncate(truncate_depth)
for truncate_depth in [0.2, 0.5, 0.8]
}
multi_show(
[G] + list(truncate_graphs.values()),
titles=['original'] + [f'truncate {d}' for d in truncate_graphs.keys()],
render_vertices=False, ncols=2
)
G = example_graphs.from_tiles(example_tilesets.platonic(4), rings=1)
gyro_graphs = {
gyro_params: G.gyro(gyro_params)
for gyro_params in [(1/4, -1/4), (1/8, -1/8), (1/2, -1/8)]
}
multi_show(
[G] + list(gyro_graphs.values()),
titles=['original'] + [f'gyro {d}' for d in gyro_graphs.keys()],
render_vertices=False, ncols=2
)
Example 4: Hyperbolic & Spherical tilings¶
G_spherical = example_graphs.from_tiles(example_tilesets.curved_zip(5, 3), rings=3)
G_spherical_ambo = G_spherical.ambo()
# delete some faces to make the result nicer
# more precisely, we delete the five faces which have >2 edges on the border.
def n_edges_on_border(f):
return sum(1 for h in f.halfedge_iter() if h.rev.on_border())
G_spherical_ambo_cleaned = G_spherical_ambo.copy()
G_spherical_ambo_cleaned.delete_faces(
[f for f in G_spherical_ambo_cleaned.faces if n_edges_on_border(f) > 2]
)
multi_show(
[G_spherical, G_spherical_ambo, G_spherical_ambo_cleaned],
titles=['original', 'ambo', 'cleaned'],
render_vertices=False
)
G_hyperbolic = example_graphs.from_tiles(example_tilesets.curved_zip(5, 4), rings=3)
G_hyperbolic_join = G_hyperbolic.join()
G_hyperbolic_join.convert_to_euclidean()
multi_show(
[G_hyperbolic, G_hyperbolic_join],
titles=['original', 'join'],
render_vertices=False
)
Example 5: Composition of operators¶
Conway operators can be composed, allowing for the generation of some quite complex patterns
G = example_graphs.from_tiles(
example_tilesets.curved_expand(6, 3),
rings=2,
base_tile=2
)
# Each call returns a new graph (the original is untouched), so compositions read top-to-bottom:
G2 = G.join()
G3 = G2.gyro()
# Equivalent one-liner: G.join().gyro()
multi_show(
[G, G2, G3],
titles=['original', 'join', 'gyro(join)'],
render_vertices=False, ncols=3
)
Example 6: Applying conway operators to subsets¶
faces= selects which faces an operator acts on. It can be either an explicit set of faces, or
a callable face -> bool used as a filter — handy inside chains, where the graph isn't bound to
a name.
G = example_graphs.from_tiles(
example_tilesets.curved_omnitruncate(6, 3),
rings=2,
base_tile=2
)
n_dodecagons = sum(1 for f in G.faces if f.order() == 12)
print(f'number of dodecagons: {n_dodecagons}')
# Apply the loft operator only to the dodecagonal faces, using a face-filter callable.
G_partial_loft = G.loft(faces=lambda f: f.order() == 12, delete_on_border=False)
multi_show(
[G, G_partial_loft],
titles=['original', 'lofted dodecagons'],
render_vertices=False
)
number of dodecagons: 7
Visualizing operator fundamental domains¶
Every GeometricConwayOperator is built from a small fundamental-domain graph with three corner vertices (v1, vf, v2). op.show() renders that domain, colouring elements by role:
- orange — the three triangle corners,
- red — vertices/edges marked
delete=True(removed during substitution), - green — vertices marked
join=True(collapsed if order-2 after substitution), - grey — retained elements.
Pass annotate_barycentric=True to also print each vertex's barycentric coordinates relative to (v1, vf, v2).
names = []
operators = []
for name in dir(conway):
if name.endswith('_graph'):
operators.append(getattr(conway, name)())
title = name[:-6].replace('_', ' ')
names.append(title)
# sort by number of edges which are not deleted in the fundamental domain, then by name
operators, names = zip(*sorted(
zip(operators, names),
key=lambda pair: (len([h for h in pair[0].graph.halfedges if not h.attributes.get('delete', False)]), pair[1])
))
multi_show(
operators,
titles=names,
ncols=3,
)
Going further¶
Many algorithms to construct crease patterns for origami tessellations from a tiling can be implemented as a two step process: First, apply a certain operator which adds all the necessary creases (the correct topology of the crease pattern), secondly move the vertices in the crease pattern to their correct positions. This process is used for shrink-rotate tessellations (via the shrink_rotate operator), intersecting-cylinder tessellations (via expand and lace), as well as alternating flagstones (via alternating_flagstone). Which other kinds of origami tessellations may be possible to construct this way?