Skip to content

symmetry_finder

Native symmetry detection for crystal structures.

Finds the full symmetry of a crystal from its atomic positions and lattice vectors using a hierarchical, numpy-vectorized algorithm:

  1. Metric tensor filter - keeps rotations R where R^T G R = G
  2. Translation search - finds candidate translations for each rotation
  3. Full verification - checks all atoms map to same-element partners
  4. Space group identification - feeds results into identify_standard_setting()

find_asymmetric_unit_indices(positions, elements, symops, A, atol=0.01)

Group atoms into orbits under symmetry operations and pick representatives.

Parameters:

Name Type Description Default
positions

(N, 3) fractional coordinates

required
elements

(N,) integer atomic numbers

required
symops

list of SymmetryOperation objects

required
A

(3, 3) direct lattice matrix

required
atol

Cartesian tolerance in Angstroms

0.01

Returns:

Type Description

list of indices into positions array, one per orbit

Source code in chmpy/crystal/symmetry_finder.py
def find_asymmetric_unit_indices(positions, elements, symops, A, atol=0.01):
    """Group atoms into orbits under symmetry operations and pick representatives.

    Args:
        positions: (N, 3) fractional coordinates
        elements: (N,) integer atomic numbers
        symops: list of SymmetryOperation objects
        A: (3, 3) direct lattice matrix
        atol: Cartesian tolerance in Angstroms

    Returns:
        list of indices into positions array, one per orbit
    """
    N = len(positions)
    orbit_id = np.full(N, -1, dtype=int)
    current_orbit = 0

    for i in range(N):
        if orbit_id[i] >= 0:
            continue
        orbit_id[i] = current_orbit
        # Apply all symops to atom i, find which atoms they map to
        for symop in symops:
            transformed = (symop.rotation @ positions[i] + symop.translation) % 1.0
            # Find matching atom
            diff = positions - transformed
            diff = diff - np.round(diff)
            cart_diff = diff @ A
            dist_sq = np.sum(cart_diff ** 2, axis=1)
            # Must match element
            same_el = elements == elements[i]
            dist_sq[~same_el] = np.inf
            j = np.argmin(dist_sq)
            if dist_sq[j] < atol ** 2:
                orbit_id[j] = current_orbit
        current_orbit += 1

    # Pick one representative per orbit (the lowest index)
    representatives = []
    for oid in range(current_orbit):
        members = np.where(orbit_id == oid)[0]
        representatives.append(members[0])

    return representatives

find_symmetry_operations(unit_cell, positions, elements, atol=0.01)

Find all symmetry operations of a crystal structure.

Parameters:

Name Type Description Default
unit_cell

UnitCell object

required
positions

(N, 3) fractional coordinates of all atoms in unit cell

required
elements

(N,) integer atomic numbers

required
atol

Cartesian tolerance in Angstroms

0.01

Returns:

Type Description

list[SymmetryOperation]: all detected symmetry operations

Source code in chmpy/crystal/symmetry_finder.py
def find_symmetry_operations(unit_cell, positions, elements, atol=0.01):
    """Find all symmetry operations of a crystal structure.

    Args:
        unit_cell: UnitCell object
        positions: (N, 3) fractional coordinates of all atoms in unit cell
        elements: (N,) integer atomic numbers
        atol: Cartesian tolerance in Angstroms

    Returns:
        list[SymmetryOperation]: all detected symmetry operations
    """
    G = unit_cell.metric_tensor
    A = unit_cell.direct

    # Stage 1: metric tensor filter
    compatible_rots = _metric_compatible_rotations(G, atol=1e-4)
    LOG.debug(
        "Metric tensor filter: %d / %d rotations compatible",
        len(compatible_rots), len(_CRYSTALLOGRAPHIC_ROTATIONS),
    )

    # Stage 2+3: find and verify translations for each rotation
    symops = []
    for R in compatible_rots:
        for t in _find_translations(R, positions, elements, atol, A):
            symops.append(SymmetryOperation(R, t))

    LOG.debug("Found %d symmetry operations", len(symops))
    return symops