Source code for mag4.magnon.kpath

"""High-symmetry k-path generation (auto via pymatgen, or user-supplied label string)."""

from __future__ import annotations

import logging
import warnings
from typing import Dict, List, Tuple

import numpy as np

log = logging.getLogger(__name__)


def _clean_label(s: str) -> str:
    """Normalise a high-symmetry point label to unicode Γ."""
    for v in ("\\Gamma", "GAMMA", "Gamma", "GM"):
        s = s.replace(v, "Γ")
    if s == "G":
        s = "Γ"
    return s


def _build_custom_kpath_arrays(custom_str: str,
                                hs_frac: Dict,
                                rec_matrix: np.ndarray,
                                n_points: int):
    """Build kpath arrays from a user-supplied label string.

    Labels are space-separated HS point names.  ``|`` marks a discontinuity.
    Example: ``"Γ M K Γ | A L H"``.
    """
    hs_norm = {_clean_label(k): np.array(v, dtype=float)
               for k, v in hs_frac.items()}

    def lookup(raw_label: str) -> np.ndarray:
        clean = _clean_label(raw_label)
        if clean in hs_norm:
            return hs_norm[clean].copy()
        for k, v in hs_norm.items():
            if k.upper() == clean.upper():
                return v.copy()
        raise ValueError(
            f"Label '{raw_label}' not found.\n"
            f"  Available: {sorted(hs_norm.keys())}\n"
            f"  Tip: use 'G' or 'Gamma' for Γ.")

    tokens   = custom_str.split()
    segments: List[List[str]] = []
    current:  List[str]       = []
    for tok in tokens:
        if tok == "|":
            if current:
                segments.append(current)
            current = []
        else:
            current.append(_clean_label(tok))
    if current:
        segments.append(current)

    if not segments:
        raise ValueError("--kpath string is empty.")

    all_frac:      List[np.ndarray]      = []
    tick_raw:      List[Tuple[int, str]] = []
    break_indices: set                   = set()

    for s_idx, seg in enumerate(segments):
        if len(seg) < 1:
            continue
        seg_start = len(all_frac)
        all_frac.append(lookup(seg[0]))
        tick_raw.append((seg_start, seg[0]))

        for i in range(1, len(seg)):
            f_prev = lookup(seg[i - 1])
            f_curr = lookup(seg[i])
            for k in range(1, n_points + 1):
                t = k / n_points
                all_frac.append(f_prev + t * (f_curr - f_prev))
            tick_raw.append((len(all_frac) - 1, seg[i]))

        if s_idx > 0:
            break_indices.add(seg_start - 1)
            break_indices.add(seg_start)

    kpoints_frac = np.array(all_frac, dtype=float)
    kpoints_cart = kpoints_frac @ rec_matrix
    seg_dists    = np.linalg.norm(np.diff(kpoints_cart, axis=0), axis=1)
    distances    = np.concatenate([[0.0], np.cumsum(seg_dists)])

    tick_positions: List[float] = []
    tick_labels:    List[str]   = []
    for idx, lbl in tick_raw:
        d = float(distances[idx])
        if tick_positions and abs(d - tick_positions[-1]) < 1e-8:
            prev = tick_labels[-1]
            if lbl != prev and lbl not in prev.split("|"):
                tick_labels[-1] = prev + "|" + lbl
        else:
            tick_positions.append(d)
            tick_labels.append(lbl)

    return (kpoints_frac, kpoints_cart, distances,
            break_indices, tick_positions, tick_labels)


[docs] def get_kpath(structure, n_points: int = 100, custom_labels: str = None) -> Dict: """Generate a high-symmetry k-path using pymatgen :class:`HighSymmKpath`. Parameters ---------- structure : pymatgen Structure n_points : int Number of points per segment. custom_labels : str, optional Space-separated label string, e.g. ``"Γ M K Γ | A L H"``. ``|`` marks a discontinuity. If ``None`` the automatic pymatgen path is used. Returns ------- dict Keys: ``kpoints_cart, kpoints_frac, distances, tick_positions, tick_labels, break_indices, prim_rec_matrix, hs_frac``. """ from pymatgen.symmetry.bandstructure import HighSymmKpath sg_num = structure.get_space_group_info()[1] with warnings.catch_warnings(): warnings.filterwarnings( "ignore", message=".*does not match the expected standard primitive.*") kpath_obj = HighSymmKpath(structure) rec = structure.lattice.reciprocal_lattice frac_list_full, labels_list_full = kpath_obj.get_kpoints( line_density=max(n_points, 10), coords_are_cartesian=False) kf_full = np.array(frac_list_full, dtype=float) hs_frac: Dict[str, np.ndarray] = {} for i, lbl in enumerate(labels_list_full): if lbl == "": continue clean = _clean_label(lbl) key = clean.split("|")[0] if key not in hs_frac: hs_frac[key] = kf_full[i] if custom_labels is not None: log.info("Using custom k-path: %s", custom_labels) (kpoints_frac, kpoints_cart, distances, break_indices, tick_positions, tick_labels) = \ _build_custom_kpath_arrays( custom_labels, hs_frac, rec.matrix, n_points) source = "custom" else: frac_list, labels_list = kpath_obj.get_kpoints( line_density=n_points, coords_are_cartesian=False) kpoints_frac = np.array(frac_list, dtype=float) kpoints_cart = kpoints_frac @ rec.matrix seg_dists = np.linalg.norm(np.diff(kpoints_cart, axis=0), axis=1) distances = np.concatenate([[0.0], np.cumsum(seg_dists)]) labeled_indices = [i for i, lb in enumerate(labels_list) if lb != ""] break_indices = set() for k in range(1, len(labeled_indices)): ia = labeled_indices[k - 1] ib = labeled_indices[k] if ib == ia + 1: d_step = float(np.linalg.norm( kpoints_cart[ib] - kpoints_cart[ia])) if d_step > 1e-6: break_indices.add(ia) break_indices.add(ib) tick_positions, tick_labels_raw = [], [] for i, lbl in enumerate(labels_list): if lbl == "": continue if tick_positions and abs(distances[i] - tick_positions[-1]) < 1e-8: prev = tick_labels_raw[-1] if lbl != prev and lbl not in prev.split("|"): tick_labels_raw[-1] = prev + "|" + lbl else: tick_positions.append(float(distances[i])) tick_labels_raw.append(lbl) tick_labels = [_clean_label(lb) for lb in tick_labels_raw] source = "auto" if sg_num <= 2: latt_type = "triclinic" elif sg_num <= 15: latt_type = "monoclinic" elif sg_num <= 74: latt_type = "orthorhombic" elif sg_num <= 142: latt_type = "tetragonal" elif sg_num <= 167: latt_type = "trigonal" elif sg_num <= 194: latt_type = "hexagonal" else: latt_type = "cubic" log.info("k-path (%s) | %s | %d pts | %d ticks", source, latt_type, len(kpoints_frac), len(tick_labels)) log.info(" Path: %s", " -- ".join(tick_labels)) log.info(" Breaks: %d", len(break_indices) // 2) log.info(" Available HS points:") for lbl, frac in sorted(hs_frac.items()): log.info(" %-10s frac=(%+.6f, %+.6f, %+.6f) " "cart=(%+.4f, %+.4f, %+.4f) A-1", lbl, frac[0], frac[1], frac[2], *(frac @ rec.matrix)) return dict( kpoints_cart = kpoints_cart, kpoints_frac = kpoints_frac, distances = distances, tick_positions = tick_positions, tick_labels = tick_labels, break_indices = break_indices, prim_rec_matrix = rec.matrix, hs_frac = hs_frac, )