Skip to content

Group algorithms and space-group symmetries for `Lattice`

Vicentini Filippo requested to merge github/fork/attila-i-szabo/groups into master

Created by: attila-i-szabo

This PR implements most of the features discussed in #703 (closed) pertaining to symmorphic space groups.

New stuff and changes

Groups

  • New module netket.utils.group to replace semigroup.py: it takes care of everything related to symmetry groups
    • contains SemiGroup, identity, Element, PermutationGroup, Permutation with unchanged API
    • some new features, e.g. Permutation objects can carry an arbitrary name
  • New class Group: base class for all group-like objects that are guaranteed to satisfy group axioms
    • methods to calculate inverse mapping, times table, conjugacy classes, character tables (using Burnside's algorithm) for a generic group
    • equality checking via the function _canonical() provided by subclasses: this must return an integer array for all group elements such that equal arrays imply equal group elements (this is a property of the specific group classes to allow them to handle Identity as they see fit)
  • New class PGSymmetry: represents a point group symmetry around the origin, specified by a transformation matrix
    • autogenerated name describes transformation in human-readable form
  • New class PointGroup: stores PGSymmetry objects
  • All crystallographically relevant point groups provided in submodules planar (2D), axial, cubic (3D)
  • New class SpaceGroupBuilder (in netket.graph)
    • translates PointGroups into PermutationGroups acting on a particular Lattice
    • generates the translation group of a Lattice as PermutationGroups (with sensible names attached)
    • hence generates space groups as PermutationGroups
    • helps calculate the character table of the space group intuitively (i.e., calculates irreps consistent with a given wave vector)

Graphs

  • The custom rotation etc. groups of Lattice are removed in favour of using the above machinery

  • NetworkX.automorphisms() changed to only return an array of permutation indices; the PermutationGroup is made by a free-floating method in symmetry. (SpaceGroupBuilder needs to use Lattice, which means that having a reference to anything in symmetry within Lattice produces a circular import via symmetry/__init__py.) It is also deprecated and should eventually be replaced by a hidden method that feeds into symmetry.automorphism_group(): it makes little sense to have this single piece of symmetry functionality outside symmetry. Alternatively, SpaceGroupBuilder could live in the graph module and be blended into the functionality of Lattice, similar to how automorphisms() behaves now.

  • Lattice is given several new methods:

    • space_group_builder() returns a SpaceGroupBuilder object (see above) corresponding to the translations of the lattice and the supplied point group. The Lattice constructor also takes a PointGroup argument that is cached as a default point group.
    • point_group() returns the representation of its PointGroup argument or the default point group as a PermutationGroup
    • rotation_group() picks out the rotations (determinant of rotation matrix is +1)
    • translation_group() returns the group of lattice translations as a PermutationGroup. It takes an optional argument to specify the axes along which to translate
    • space_group() is the semidirect product translation_group() @ point_group().

    All of these are convenience wrappers around methods of space_group_builder().

  • The "hashing logic" in Lattice is tidied up and extended to wave vectors (needed in SpaceGroupBuilder). It now honours periodic and open BCs.

  • The Grid class is removed and replaced by functions of the same calling sequence that return Lattices. (The space-group functionality is built around Lattices, so it is better to focus on improving that one API rather than developing several independent ones.)

    • This breaks Grid's ability to colour its edges by direction. A more flexible constructor for Lattice will solve this problem.
    • planar_rotation() and axis_reflection() are dropped as they were in Lattice
    • The name space_group() was used incorrectly: instead of that and lattice_group(), we have Lattice.point_group() and Lattice.space_group(). Deprecation would be hard, since one of the names is reused in a different meaning.
    • point_group() only returns symmetries that leave the origin in place. This is different from the original behaviour for open BC axes (could be fixed by allowing nonsymmorphic point groups, but I need more reason than this to implement those).
  • Specialised constructors for triangle, honeycomb and kagome lattices are added.

Odds and ends

  • The "hashing logic" (used both in PointGroup and Lattice) is moved into netket.utils.float_utils and fine-tuned. It also supports a mix of periodic and open boundary conditions. I've also added
    • a function that prunes nearly-zero real and imaginary parts from an array;
    • a function that checks whether elements of an array are nearly integers.
  • netket.jax.logsumexp is added, which extends the functionality of JAX logsumexp to handle complex numbers well (i.e., it forces the output to be complex and doesn't error out on complex inputs/outputs)
  • Functions to project the outputs of DenseSymm and DenseEquivariant onto irreps using their characters. The default flavour uses logsumexp, but there is one with plain sums too.

Typical workflows

A simple workflow, without character tables

We just want to generate the space group of a Lattice given a point group we know it's invariant under:

from netket.utils import group
from netket.graph import Lattice

graph = Lattice(basis_vectors = [[1,0],[0.5,0.75**0.5]], extent = (6,6)) # triangle lattice
space_group = graph.space_group(group.planar.D(6))

The resulting space_group is a PermutationGroup that can be used directly in a GCNN, for instance. Alternatively, we can use the premade triangular lattice that is loaded with the D_6 group:

from netket.graph import TriangularLattice

graph = TriangularLattice([6,6])
space_group = graph.space_group()

Using character tables

For this, one needs a basic appreciation of how crystallographic character tables are constructed. They can be described in terms of a wave vector (or rather a star of symmetry-related wave vectors) and the irreps of the corresponding little group (the subgroup of the point group that leaves the wave vector unchanged). The latter can be read off from a human-readable character table one can generate in an interactive session:

from netket.utils import group
from netket.graph import TriangularLattice
from math import pi

graph = TriangularLattice([6,6]) 
sgb = graph.space_group_builder()

k = [4*pi/3,0] # corner of the hexagonal BZ
sgb.little_group(k) 

> PointGroup(elems=[Id(), Rot(120°), Rot(-120°), Refl(0°), Refl(-60°), Refl(60°)], ndim=2)

sgb.little_group(k).character_table_readable()

> (['1xId()', '2xRot(120°)', '3xRefl(0°)'], 
array([[ 1.,  1.,  1.],
       [ 1.,  1., -1.],
       [ 2., -1.,  0.]]))

The first output confirms that the little group of D_6 at the corner of the Brillouin zone is D_3, whose known character table is generated by the second command; given the labels in the first part of the output, they are easy to match to the characters in these tables, so we can look up their physical/geometrical meaning. Any of these can be turned into an irrep of the full space group using SpaceGroupBuilder: in fact, it generates all of them as a 2D array (in the same order as the irreps printed above), so we'd write something like

chi = sgb.space_group_irreps(k)[2] # [2] selects the "E" irrep
# ...
# in the definition of the GCNN
return irrep_project_logsumexp(output, chi)

To do

  • Writing tests. I tested most of the stuff manually and it seems to work in all cases, but of course it has to be more systematic.
  • Writing docs. Probably the best place for the kind of workflow docs you see above would be in @chrisrothUT's tutorial on GCNNs, and #700 will be updated with how the abstract stuff gets implemented here.
  • Checking if the stuff that got caught up in an earlier git-rebase (see first 2 commits) affects the behaviour of struct.dataclass. Everything seems to work fine, so I'm not too worried, but @PhilipVinc could you perhaps check and suggest what I should do?
  • Non-symmorphic groups? I've given some thought to it, PointGroup wouldn't be too hard to extend, the main question is whether the automatic construction of character tables generalises nicely. I have a hunch that it does, but I would need some downtime with a group theory textbook to make sure. Probably left for another PR
  • Extending Lattice so it can have further-neighbour and coloured edges (this is functionality that is lost from the new Grid for instance). I just flag this up, but it can wait.

An Easter egg

The NetworkX algorithm really looks for all automorphisms:

lattice = nk.graph.Square(4)
len(symmetry.automorphism_group(lattice))
> 384
len(symmetry.space_group(lattice, symmetry.planar.D(4)))
> 128

It turns out that a 4x4 square lattice with PBC is isomorphic to a 2^4 hypercube, which has many more symmetries. E.g., you can check that this maps nearest neighbours to nearest neighbours without making any geometrical sense:

 0,  1,  5,  4
 3,  2,  6,  7
15, 14, 10, 11
12, 13,  9,  8

This is an interesting caveat for using NetworkX graph matching. PS. The 3×3 triangle lattice turns out to have 1296 isomorphisms, of which only 108 are space-group symmetries!

Merge request reports