Module weavingspace.tiling_geometries

Functions for setting up a TileUnit with various tile geometries. Some care is required in adding new functions that use exisitng ones to get the sequence of setup operations right. Modify with care!

The available tilings can be viewed on this page.

These tilings (and many many more!) are discussed in

Grunbaum B, Shephard G C, 1987 Tilings and Patterns (W. H. Freeman and Company, New York)

A more accessible introduction is

Kaplan C S, 2002 Computer Graphics and Geometric Ornamental Design, PhD thesis, University of Washington, Seattle, WA, https://cs.uwaterloo.ca/~csk/other/phd/kaplan_diss_full_print.pdf

and a more 'polished' version of that work focused on computer graphics is also available

Kaplan C S, 2009 Introductory tiling theory for computer graphics (Morgan & Claypool)

Functions

def get_4_parts_of_hexagon(unit: TileUnit) ‑> list[shapely.geometry.polygon.Polygon]
Expand source code
def get_4_parts_of_hexagon(unit: "TileUnit") -> list[geom.Polygon]:
  outer_h = tiling_utils.get_regular_polygon(unit.spacing, 6)
  inner_h = affine.scale(outer_h, 1/np.sqrt(3), 1/np.sqrt(3))
  if unit.offset == 1:
    inner_h = affine.rotate(inner_h, 30, (0, 0))
  o_hx = tiling_utils.get_corners(outer_h)
  i_hx = tiling_utils.get_corners(inner_h)
  if unit.offset == 1:
    o = []
    for p1, p2 in zip(o_hx[:-1], o_hx[1:]):
      o.extend([p1, geom.Point([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2])])
    return [
      inner_h,
      geom.Polygon([i_hx[2], i_hx[1], i_hx[0], o[11], o[0], o[2], o[3]]),
      geom.Polygon([i_hx[4], i_hx[3], i_hx[2], o[3], o[4], o[6], o[7]]),
      geom.Polygon([i_hx[0], i_hx[5], i_hx[4], o[7], o[8], o[10], o[11]])
    ]
  else:
    return [
      inner_h,
      geom.Polygon([i_hx[2], i_hx[1], i_hx[0], o_hx[0], o_hx[1], o_hx[2]]),
      geom.Polygon([i_hx[4], i_hx[3], i_hx[2], o_hx[2], o_hx[3], o_hx[4]]),
      geom.Polygon([i_hx[0], i_hx[5], i_hx[4], o_hx[4], o_hx[5], o_hx[0]])
    ]
def get_7_parts_of_hexagon(unit: TileUnit) ‑> list[shapely.geometry.polygon.Polygon]
Expand source code
def get_7_parts_of_hexagon(unit: "TileUnit") -> list[geom.Polygon]:
  outer_h = tiling_utils.get_regular_polygon(unit.spacing, 6)
  inner_h = affine.scale(outer_h, 1/np.sqrt(7), 1/np.sqrt(7))
  if unit.offset == 1:
    inner_h = affine.rotate(inner_h, 30, (0, 0))
  outer = tiling_utils.get_corners(outer_h)
  inner = tiling_utils.get_corners(inner_h)
  if unit.offset == 1:
    o = []
    for p1, p2 in zip(outer[:-1], outer[1:]):
      o.extend([p1, geom.Point([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2])])
    i = []
    for p1, p2 in zip(inner[:-1], inner[1:]):
      i.extend([p1, geom.Point([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2])])
    return [
      inner_h,
      geom.Polygon([i[2], i[0], o[11], o[0], o[1]]),
      geom.Polygon([i[4], i[2]] + o[1:4]),
      geom.Polygon([i[6], i[4]] + o[3:6]),
      geom.Polygon([i[8], i[6]] + o[5:8]),
      geom.Polygon([i[10], i[8]] + o[7:10]),
      geom.Polygon([i[0], i[10]] + o[9:])
    ]
  else:
    return [
      inner_h,
      geom.Polygon([inner[1], inner[0], outer[0], outer[1]]),
      geom.Polygon([inner[2], inner[1], outer[1], outer[2]]),
      geom.Polygon([inner[3], inner[2], outer[2], outer[3]]),
      geom.Polygon([inner[4], inner[3], outer[3], outer[4]]),
      geom.Polygon([inner[5], inner[4], outer[4], outer[5]]),
      geom.Polygon([inner[0], inner[5], outer[5], outer[0]])
    ]
def get_9_parts_of_hexagon(unit: TileUnit) ‑> list[shapely.geometry.polygon.Polygon]
Expand source code
def get_9_parts_of_hexagon(unit: "TileUnit") -> list[geom.Polygon]:
  c = geom.Point(0, 0)
  outer_h = tiling_utils.get_regular_polygon(unit.spacing, 6)
  inner_h = affine.scale(outer_h, 1/np.sqrt(3), 1/np.sqrt(3))
  if unit.offset == 1:
    inner_h = affine.rotate(inner_h, 30, (0, 0))
  outer = tiling_utils.get_corners(outer_h)
  inner = tiling_utils.get_corners(inner_h)
  if unit.offset == 1:
    o = []
    for p1, p2 in zip(outer[:-1], outer[1:]):
      o.extend([p1, geom.Point([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2])])
    i = []
    for p1, p2 in zip(inner[:-1], inner[1:]):
      i.extend([p1, geom.Point([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2])])
    return [
      geom.Polygon([c, i[1], i[2], i[4], i[5]]),
      geom.Polygon([c, i[5], i[6], i[8], i[9]]),
      geom.Polygon([c, i[9], i[10], i[0], i[1]]),
      geom.Polygon([o[0], o[1], i[2], i[0], o[11]]),
      geom.Polygon([o[2], o[3], i[4], i[2], o[1]]),
      geom.Polygon([o[4], o[5], i[6], i[4], o[3]]),
      geom.Polygon([o[6], o[7], i[8], i[6], o[5]]),
      geom.Polygon([o[8], o[9], i[10], i[8], o[7]]),
      geom.Polygon([o[10], o[11], i[0], i[10], o[9]])
    ]
  else:
    return [
      geom.Polygon([c, inner[0], inner[1], inner[2]]),
      geom.Polygon([c, inner[2], inner[3], inner[4]]),
      geom.Polygon([c, inner[4], inner[5], inner[0]]),
      geom.Polygon([inner[1], inner[0], outer[0], outer[1]]),
      geom.Polygon([inner[2], inner[1], outer[1], outer[2]]),
      geom.Polygon([inner[3], inner[2], outer[2], outer[3]]),
      geom.Polygon([inner[4], inner[3], outer[3], outer[4]]),
      geom.Polygon([inner[5], inner[4], outer[4], outer[5]]),
      geom.Polygon([inner[0], inner[5], outer[5], outer[0]])
    ]
def setup_archimedean(unit: TileUnit)
Expand source code
def setup_archimedean(unit:"TileUnit") -> None:
  """The Archimedean 'regular tilings. See https://en.wikipedia.org/wiki/List_of_Euclidean_uniform_tilings#Convex_uniform_tilings_of_the_Euclidean_plane

  Many of these are most easily constructed as duals of the Laves tilings.

  Some are not yet implemented:

  (3.3.3.4.4) is kind of weird (stripes of triangles and squares) so can't be
  bothered with it. Perhaps as a 5-variable option we'll get to it in time.

  Args:
    unit (TileUnit):  the TileUnit to setup.
  """
  if unit.code == "3.3.3.3.3.3":
    _setup_base_tile(unit, TileShape.TRIANGLE)
    _setup_none_tile(unit)
    return
  if unit.code == "3.3.3.3.6":
    setup_laves(unit)
    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
    unit.setup_regularised_prototile_from_tiles()
    return
  elif unit.code == "3.3.3.4.4":
    print(f"The code [{unit.code}] is unsupported.")
  elif unit.code == "3.3.4.3.4":
    # this is an attractive 6-colourable triangles and squares tiling
    setup_laves(unit)
    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
    unit.setup_regularised_prototile_from_tiles()
    return
  elif unit.code == "3.4.6.4":
    setup_laves(unit)
    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
    unit.setup_regularised_prototile_from_tiles()
    return
  elif unit.code == "3.6.3.6":
    setup_laves(unit)
    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
    unit.setup_regularised_prototile_from_tiles()
    return
  elif unit.code == "3.12.12":
    # nice! we can make dodecagons without having to think too hard
    # simply use the dual code. (Although really... it probably
    # would've been easier to make the dodecagon... other than
    # calculating the scale relative to the hexagon base tile!)
    setup_laves(unit)
    unit.setup_vectors()
    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
    unit.setup_regularised_prototile_from_tiles()
    return
  elif unit.code == "4.4.4.4":
    _setup_base_tile(unit, TileShape.RECTANGLE)
    _setup_none_tile(unit)
    return
  elif unit.code == "4.6.12":
    # more dodecagons for free!
    setup_laves(unit)
    unit.setup_vectors()
    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
    unit.setup_regularised_prototile_from_tiles()
    return
  elif unit.code == "4.8.8":
    # this is the octagon and square tiling
    setup_laves(unit)
    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
    unit.setup_regularised_prototile_from_tiles()
    return
  elif unit.code == "6.6.6":
    _setup_base_tile(unit, TileShape.HEXAGON)
    _setup_none_tile(unit)
    return
  else:
    print(f"[{unit.code}] is not a valid Laves code.")

  unit.tiling_type = None
  _setup_none_tile(unit)
  return

The Archimedean 'regular tilings. See https://en.wikipedia.org/wiki/List_of_Euclidean_uniform_tilings#Convex_uniform_tilings_of_the_Euclidean_plane

Many of these are most easily constructed as duals of the Laves tilings.

Some are not yet implemented:

(3.3.3.4.4) is kind of weird (stripes of triangles and squares) so can't be bothered with it. Perhaps as a 5-variable option we'll get to it in time.

Args

unit : TileUnit
the TileUnit to setup.
def setup_cairo(unit: TileUnit)
Expand source code
def setup_cairo(unit:"TileUnit") -> None:
  """Sets up the Cairo tiling. King of tilings. All hail the Cairo tiling.
  This code shows how a 'handcoded' set of geometries can be applied.

  Note that it is advisable to avoid intersection and union operations where
  possible, as it often yields floating point mismatches that can be hard
  to repair! (This tiling can be relatively conveniently generated by
  dissecting a square in 4 quarters at a 30 degree angle to the sides and
  then reflecting and rotating copies and joing them back together. But
  floating point issues make that very messy indeed. Much better to make
  the geometries 'pure'.)

  Args:
    unit (TileUnit):  the TileUnit to setup.
  """
  _setup_base_tile(unit, TileShape.RECTANGLE)  # a square
  d = unit.spacing
  x = d / 2 / (np.cos(np.radians(15)) + np.cos(np.radians(75)))
  # the following is just the geometry, it is what it is...
  # points are (more or less)
  #
  #         3
  #     2
  #            4
  #     1    0
  #
  # then rotate -15 and make 4 copies at 90 degree rotations
  p1 = geom.Polygon([(x, 0), (0, 0), (0, x),
             (x * np.sqrt(3) / 2, x + x / 2),
             (x * (1 + np.sqrt(3)) / 2, x * (3 - np.sqrt(3)) / 2)])
  p1 = affine.rotate(p1, -15, (0, 0))
  p2 = affine.rotate(p1, 90, (0, 0))
  p3 = affine.rotate(p1, 180, (0, 0))
  p4 = affine.rotate(p1, 270, (0, 0))

  # now move them so they are arranged as a hexagon centered on the tile
  p1 = affine.translate(p1, -unit.spacing / 2, 0)
  p2 = affine.translate(p2, unit.spacing / 2, 0)
  p3 = affine.translate(p3, unit.spacing / 2, 0)
  p4 = affine.translate(p4, -unit.spacing / 2, 0)

  unit.tiles = gpd.GeoDataFrame(
    data = {"tile_id": list("abcd")}, crs = unit.crs,
    geometry = gpd.GeoSeries([p1, p2, p3, p4]))
  unit.setup_regularised_prototile_from_tiles()

Sets up the Cairo tiling. King of tilings. All hail the Cairo tiling. This code shows how a 'handcoded' set of geometries can be applied.

Note that it is advisable to avoid intersection and union operations where possible, as it often yields floating point mismatches that can be hard to repair! (This tiling can be relatively conveniently generated by dissecting a square in 4 quarters at a 30 degree angle to the sides and then reflecting and rotating copies and joing them back together. But floating point issues make that very messy indeed. Much better to make the geometries 'pure'.)

Args

unit : TileUnit
the TileUnit to setup.
def setup_hex_colouring(unit: TileUnit)
Expand source code
def setup_hex_colouring(unit:"TileUnit") -> None:
  """3, 4, and 7 colourings of a regular array of hexagons.

  Args:
    unit (TileUnit):  the TileUnit to setup.
  """
  hexagon = tiling_utils.get_regular_polygon(unit.spacing / np.sqrt(unit.n), 6)
  if unit.n == 3:
    # Point up hex at '*' displaced to 3 positions:
    #      2
    #      *
    #    3   1
    _setup_base_tile(unit, TileShape.HEXAGON)
    hexagon = affine.rotate(hexagon, 30, origin = (0, 0))
    # Copy and translate to alternate corners
    corners = [p for i, p in enumerate(hexagon.exterior.coords)
           if i in (0, 2, 4)]
    hexes = [affine.translate(hexagon, p[0], p[1]) for p in corners]
  elif unit.n == 4:
    # Point up hex at '*' displaced to 4 positions:
    #      2
    #     3*1
    #      4
    _setup_base_tile(unit, TileShape.DIAMOND)
    hexagon = affine.rotate(hexagon, 30, origin = (0, 0))
    hex1 = affine.translate(hexagon, unit.spacing / 4, 0)
    hex2 = affine.translate(hexagon, 0, unit.spacing * np.sqrt(3) / 4)
    hex3 = affine.translate(hexagon, -unit.spacing / 4, 0)
    hex4 = affine.translate(hexagon, 0, -unit.spacing * np.sqrt(3) / 4)
    hexes = [hex1, hex2, hex3, hex4]
  elif unit.n == 7:  # the 'H3' tile
    # Make a hexagon and displace in the direction of its
    # own 6 corners, scaled as needed
    _setup_base_tile(unit, TileShape.HEXAGON)
    rotation = np.degrees(np.arctan(1 / 3 / np.sqrt(3)))
    corners = [p for p in hexagon.exterior.coords][:-1]
    hexagon = affine.rotate(hexagon, 30)
    hexes = [hexagon] + [affine.translate(
      hexagon, x * np.sqrt(3), y * np.sqrt(3)) for x, y in corners]
    hexes = [affine.rotate(h, rotation, origin = (0, 0))
             for h in hexes]
  else:
    _setup_base_tile(unit, TileShape.HEXAGON)
    _setup_none_tile(unit)
    return

  unit.tiles = gpd.GeoDataFrame(
    data = {"tile_id": list(string.ascii_letters)[:unit.n]},
    crs = unit.crs,
    geometry = gpd.GeoSeries(hexes))
  unit.setup_regularised_prototile_from_tiles()

3, 4, and 7 colourings of a regular array of hexagons.

Args

unit : TileUnit
the TileUnit to setup.
def setup_hex_dissection(unit: TileUnit)
Expand source code
def setup_hex_dissection(unit:"TileUnit") -> None:
  """Tilings from dissection of a hexagon into parts.

  The supplied unit should have offset and n set.

  self.offset == 1 starts at midpoints, 0 at hexagon corners
  self.n is the number of slices and should be 2, 3, 4, 6 or 12.

  Args:
    unit (TileUnit):  the TileUnit to setup.
  """
  _setup_base_tile(unit, TileShape.HEXAGON)
  if unit.n == 4:
    parts = get_4_parts_of_hexagon(unit)
  elif unit.n == 7:
    parts = get_7_parts_of_hexagon(unit)
  elif unit.n == 9:
    parts = get_9_parts_of_hexagon(unit)
  unit.tiles = gpd.GeoDataFrame(
    data = {"tile_id": list(string.ascii_letters)[:unit.n]},
    crs = unit.crs,
    geometry = gpd.GeoSeries(parts))
  unit.regularised_prototile = copy.deepcopy(unit.prototile)

Tilings from dissection of a hexagon into parts.

The supplied unit should have offset and n set.

self.offset == 1 starts at midpoints, 0 at hexagon corners self.n is the number of slices and should be 2, 3, 4, 6 or 12.

Args

unit : TileUnit
the TileUnit to setup.
def setup_hex_slice(unit: TileUnit)
Expand source code
def setup_hex_slice(unit:"TileUnit") -> None:
  """Tilings from radial slices of a hexagon into 2, 3, 4, 6 or 12 slices.

  The supplied unit should have offset and n set.

  self.offset == 1 starts at midpoints, 0 at hexagon corners
  self.n is the number of slices and should be 2, 3, 4, 6 or 12.

  Again, construction avoids intersection operations where possible.

  Args:
    unit (TileUnit):  the TileUnit to setup.
  """
  _setup_base_tile(unit, TileShape.HEXAGON)
  hexagon = tiling_utils.get_regular_polygon(unit.spacing, 6)
  # note that shapely coords includes the first point at beginning
  # and end - very convenient!
  v = list(hexagon.exterior.coords)
  # midpoints
  m = [((p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2)
          for p1, p2 in zip(v[:-1], v[1:])]
  # chain into a list of corner, midpoint, corner, midpoint..., i.e.
  #
  #           6  5  4
  #          7       3
  #         8         2
  #          9       1
  #           10 11 0
  #
  # then depending on number of slices and offset pick out points.
  # Do this explicitly not by rotation to avoid gaps.
  p = list(itertools.chain(*zip(v[:-1], m)))
  if unit.n == 2:
    slices = (
      [geom.Polygon([p[0], p[2], p[4], p[6]]),
       geom.Polygon([p[6], p[8], p[10], p[0]])]
      if unit.offset == 0 else
      [geom.Polygon([p[1], p[2], p[4], p[6], p[7]]),
       geom.Polygon([p[7], p[8], p[10], p[0], p[1]])])
  elif unit.n == 3:
    slices = (
      [geom.Polygon([p[0], p[2], p[4], (0, 0)]),
       geom.Polygon([p[4], p[6], p[8], (0, 0)]),
       geom.Polygon([p[8], p[10], p[0], (0, 0)])]
      if unit.offset == 0 else
      [geom.Polygon([p[1], p[2], p[4], p[5], (0, 0)]),
       geom.Polygon([p[5], p[6], p[8], p[9], (0, 0)]),
       geom.Polygon([p[9], p[10], p[0], p[1], (0, 0)])])
  elif unit.n == 4:
    slices = (
      [geom.Polygon([p[0], p[2], p[3], (0, 0)]),
       geom.Polygon([p[3], p[4], p[6], (0, 0)]),
       geom.Polygon([p[6], p[8], p[9], (0, 0)]),
       geom.Polygon([p[9], p[10], p[0], (0, 0)])]
      if unit.offset == 0 else
      [geom.Polygon([p[1], p[2], p[4], (0, 0)]),
       geom.Polygon([p[4], p[6], p[7], (0, 0)]),
       geom.Polygon([p[7], p[8], p[10], (0, 0)]),
       geom.Polygon([p[10], p[0], p[1], (0, 0)])])
  elif unit.n == 6:
    slices = (
      [geom.Polygon([p[0], p[2], (0, 0)]),
       geom.Polygon([p[2], p[4], (0, 0)]),
       geom.Polygon([p[4], p[6], (0, 0)]),
       geom.Polygon([p[6], p[8], (0, 0)]),
       geom.Polygon([p[8], p[10], (0, 0)]),
       geom.Polygon([p[10], p[0], (0, 0)])]
      if unit.offset == 0 else
      [geom.Polygon([p[1], p[2], p[3], (0, 0)]),
       geom.Polygon([p[3], p[4], p[5], (0, 0)]),
       geom.Polygon([p[5], p[6], p[7], (0, 0)]),
       geom.Polygon([p[7], p[8], p[9], (0, 0)]),
       geom.Polygon([p[9], p[10], p[11], (0, 0)]),
       geom.Polygon([p[11], p[0], p[1], (0, 0)])])
  elif unit.n == 12:
    if unit.offset == 0:
      ids = [i for i in range(12)] + [0]
      slices = [geom.Polygon([p[i], p[j], (0, 0)])
                for i, j in zip(ids[:-1], ids[1:])]
    else:
      x = (np.sin(np.pi/6) - np.sin(np.pi/12)) / 6
      base = [x, 1/6 - x]
      steps = base
      for i in range(1, 6):
        steps = steps + [x + i/6 for x in base]
      steps = [steps[-1] - 1] + steps
      slices = [tiling_utils.get_polygon_sector(hexagon, p1, p2)
                for p1, p2 in zip(steps[:-1], steps[1:])]

  unit.tiles = gpd.GeoDataFrame(
    data = {"tile_id": list(string.ascii_letters)[:unit.n]},
    crs = unit.crs,
    geometry = gpd.GeoSeries(slices))
  unit.regularised_prototile = copy.deepcopy(unit.prototile)

Tilings from radial slices of a hexagon into 2, 3, 4, 6 or 12 slices.

The supplied unit should have offset and n set.

self.offset == 1 starts at midpoints, 0 at hexagon corners self.n is the number of slices and should be 2, 3, 4, 6 or 12.

Again, construction avoids intersection operations where possible.

Args

unit : TileUnit
the TileUnit to setup.
def setup_laves(unit: TileUnit)
Expand source code
def setup_laves(unit:"TileUnit") -> None:
  """The Laves tilings. See https://en.wikipedia.org/wiki/List_of_Euclidean_uniform_tilings#Laves_tilings.

  These are all isohedral, but mostly not regular polygons. We
  prioritise them over the Archimedean tilings because being
  isohedral all tiles are the same size. Several are hex
  dissections and setup is delegated accordingly.

  Args:
    unit (TileUnit):  the TileUnit to setup.
  """
  if unit.code == "3.3.3.3.3.3":
    # this is the regular hexagons
    _setup_base_tile(unit, TileShape.HEXAGON)
    _setup_none_tile(unit)
    return
  if unit.code == "3.3.3.3.6":
    # this one needs its own code
    _setup_laves_33336(unit)
    return
  elif unit.code == "3.3.3.4.4":
    print(f"The code [{unit.code}] is unsupported.")
  elif unit.code == "3.3.4.3.4":
    # king of tilings!
    setup_cairo(unit)
    return
  elif unit.code == "3.4.6.4":
    # the hex 6-dissection
    unit.n = 6
    unit.offset = 1
    setup_hex_slice(unit)
    return
  elif unit.code == "3.6.3.6":
    # hex 3-dissection (also a cube weave!)
    unit.n = 3
    unit.offset = 0
    setup_hex_slice(unit)
    return
  elif unit.code == "3.12.12":
    # again this one needs its own
    _setup_laves_31212(unit)
    return
  elif unit.code == "4.4.4.4":
    # square grid
    _setup_base_tile(unit, TileShape.RECTANGLE)
    _setup_none_tile(unit)
    return
  elif unit.code == "4.6.12":
    # hex 12-dissection
    unit.n = 12
    unit.offset = 0
    setup_hex_slice(unit)
    return
  elif unit.code == "4.8.8":
    # this one needs its own (a 4-dissection of the square)
    # perhaps to be added as a category later...
    _setup_laves_488(unit)
    return
  elif unit.code == "6.6.6":
    # triangles
    _setup_base_tile(unit, TileShape.TRIANGLE)
    _setup_none_tile(unit)
  else:
    print(f"[{unit.code}] is not a valid Laves code.")

  unit.tiling_type = None
  _setup_none_tile(unit)
  return

The Laves tilings. See https://en.wikipedia.org/wiki/List_of_Euclidean_uniform_tilings#Laves_tilings.

These are all isohedral, but mostly not regular polygons. We prioritise them over the Archimedean tilings because being isohedral all tiles are the same size. Several are hex dissections and setup is delegated accordingly.

Args

unit : TileUnit
the TileUnit to setup.
def setup_square_colouring(unit: TileUnit)
Expand source code
def setup_square_colouring(unit:"TileUnit") -> None:
  """Colourings of a regular array of squares. Only supports n = 5 at present
  but we need a n=5 option

  Args:
    unit (TileUnit):  the TileUnit to setup.
  """
  sq = tiling_utils.get_regular_polygon(unit.spacing, 4)
  if unit.n == 5:
    _setup_base_tile(unit, TileShape.RECTANGLE)

    # Copy and translate square
    tr = [(0, 0), (unit.spacing, 0), (0, unit.spacing),
        (-unit.spacing, 0), (0, -unit.spacing)]
    squares = [affine.translate(sq, v[0], v[1]) for v in tr]
    squares = [affine.scale(sq, 1 / np.sqrt(5), 1 / np.sqrt(5),
                origin = (0, 0)) for sq in squares]
    rotation = np.degrees(np.arctan2(1, 2))
    squares = [affine.rotate(sq, rotation, origin = (0, 0))
           for sq in squares]
  else:
    _setup_base_tile(unit, TileShape.RECTANGLE)
    _setup_none_tile(unit)
    return

  unit.tiles = gpd.GeoDataFrame(
    data = {"tile_id": list(string.ascii_letters)[:unit.n]},
    crs = unit.crs,
    geometry = gpd.GeoSeries(squares))
  unit.setup_regularised_prototile_from_tiles()

Colourings of a regular array of squares. Only supports n = 5 at present but we need a n=5 option

Args

unit : TileUnit
the TileUnit to setup.