Skip to content

Give a lattice its symmetry

A lattice's symmetry decides its irreducible Brillouin zone, and so how much of reciprocal space a grid must cover. brille.Lattice takes it in any of four forms. The code here comes from symmetry.py, which the tests run.

By name

Give a Hall symbol or a Hermann-Mauguin symbol as spacegroup:

in docs/how-to/symmetry.md, and the tests run this script.
"""
import numpy as np

from brille import BrillouinZone, Lattice

a = 5.69   # angstrom, about NaCl's lattice constant
by_hall = Lattice(([a, a, a], [90, 90, 90]), spacegroup="-F 4 2 3")
by_hermann_mauguin = Lattice(([a, a, a], [90, 90, 90]), spacegroup="Fm-3m")

Names are matched ignoring spaces and subscript marks, so P 2_1/c, P21/c and P 21/c are the same. For a space group with more than one setting, give the choice as well, spacegroup=("P 2/m", "b"), or the full Hermann-Mauguin symbol that names it, P 1 2/m 1: here, the unique axis along b. A string that is none of these is refused with a ValueError.

By generators

A HallSymbol decodes a Hall symbol into the generators of its space group, and a lattice accepts those as symmetry:

from brille import HallSymbol

generators = HallSymbol("-F 4 2 3").generators
by_generators = Lattice(([a, a, a], [90, 90, 90]), symmetry=generators)
print(f"{generators.size} generators make {generators.generate().size} operations")
11 generators make 192 operations

By explicit operations

Give every operation as a rotation matrix, in units of the real-space basis vectors, and a translation, as fractions of them. This is the form that spglib returns for a cell, so it is the route for a primitive cell from a phonon calculation:

operations = generators.generate()
rotations, translations = operations.W, operations.w   # (N, 3, 3) integers, (N, 3)
by_operations = Lattice(([a, a, a], [90, 90, 90]), symmetry=(rotations, translations))

From a CIF file

A lattice, or a Symmetry, reads operations written as CIF x, y, z strings, separated by ;; generators are enough:

from brille import Symmetry

# one generator of Pnma, as it might appear in a CIF file's _symmetry_equiv_pos_as_xyz
mirror = Symmetry("x, 1/2-y, 1/2+z")
same = Symmetry([[[1, 0, 0], [0, -1, 0], [0, 0, 1]]], [[0, 1 / 2, 1 / 2]])
print(f"xyz and matrix forms are the same: {mirror == same}")
orthorhombic = Lattice(([4.0, 5.0, 6.0], [90, 90, 90]), symmetry="x,y,z;-x,-y,-z;x,1/2-y,1/2+z")

Check the result

All four routes give the same zone:

for name, lattice in (("Hall symbol", by_hall), ("Hermann-Mauguin", by_hermann_mauguin),
                      ("generators", by_generators), ("operations", by_operations)):
    zone = BrillouinZone(lattice)
    print(f"{name:16s} {len(lattice.pointgroup.W):3d} point operations, "
          f"irreducible zone {zone.ir_polyhedron.volume:.6f} Å⁻³")
Hall symbol       48 point operations, irreducible zone 0.112207 Å⁻³
Hermann-Mauguin   48 point operations, irreducible zone 0.112207 Å⁻³
generators        48 point operations, irreducible zone 0.112207 Å⁻³
operations        48 point operations, irreducible zone 0.112207 Å⁻³

Eigenvectors and centred cells

A grid's eigenvectors describe the atoms of one primitive cell, primitive_basis. For a lattice given by a centred conventional cell (A, B, C, F, I or R), that is the first atom of each group of centring copies in the basis you give, a half, a third or a quarter of it:

# NaCl's conventional cell: Na at the face-centring points, Cl half a cell along
centring = np.array([[0, 0, 0], [0, 0.5, 0.5], [0.5, 0, 0.5], [0.5, 0.5, 0]])
positions = np.vstack([centring, (centring + 0.5) % 1])
nacl = Lattice(([a, a, a], [90, 90, 90]), spacegroup="Fm-3m", basis=(positions, [0] * 4 + [1] * 4))
print(f"{len(nacl.basis.positions)} atoms in the cell given, "
      f"{len(nacl.primitive_basis.positions)} in the primitive cell a grid's eigenvectors describe")
8 atoms in the cell given, 2 in the primitive cell a grid's eigenvectors describe

Give the lattice the conventional cell's full basis, and the grid the eigenvectors of the primitive cell, for those atoms in that order, in the cell phase convention (see the phase convention).

If your code computed the conventional cell instead, the grid also takes its eigenvectors as they are, for every atom of the basis, and interpolates all of that cell's modes. These are the primitive cell's modes at q folded together with those at the wavevectors the larger cell cannot tell from q. To keep only the primitive cell's modes, convert them first: the primitive cell's modes at q folded together with conventional_to_primitive picks out the modes at q and the primitive cell's atoms:

from brille.utils import conventional_to_primitive

values, vectors = conventional_to_primitive(lattice, grid.rlu, values, vectors)
grid.fill(values, value_elements, vectors, vector_elements)

Filling a grid with the conventional cell's eigenvectors is refused when interpolating, with an error that says so.