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.