Return the octagonal tiling graph (4.8.8, truncated square tiling,
    truncated quadrille) of MxNx8 nodes

    The 'positions' node attribute gives node coordinates for the octagonal
    tiling. (Nodes are located on a square lattice, and edge lengths are
    uneven)

def octagonal_tiling_graph(M: int, N: int) -> nx.Graph:
    """Return the octagonal tiling graph (4.8.8, truncated square tiling,
    truncated quadrille) of MxNx8 nodes

    The 'positions' node attribute gives node coordinates for the octagonal
    tiling. (Nodes are located on a square lattice, and edge lengths are
    uneven)
    """

    grp = nx.Graph()
    octogon = [[(1, 0), (0, 1)],
               [(0, 1), (0, 2)],
               [(0, 2), (1, 3)],
               [(1, 3), (2, 3)],
               [(2, 3), (3, 2)],
               [(3, 2), (3, 1)],
               [(3, 1), (2, 0)],
               [(2, 0), (1, 0)]]
    left = [[(1, 0), (1, -1)], [(2, 0), (2, -1)]]
    up = [[(0, 1), (-1, 1)], [(0, 2), (-1, 2)]]

    for m in range(M):
        for n in range(N):
            edges = octogon
            if n != 0:
                edges = edges + left
            if m != 0:
                edges = edges + up

            for (x0, y0), (x1, y1) in edges:
                grp.add_edge((m*4+x0, n*4+y0), (m*4+x1, n*4+y1))

    positions = {node: node for node in grp}
    nx.set_node_attributes(grp, positions, 'positions')

    return grp