1# coding: utf-8
2"""This module defines objects describing the sampling of the Brillouin Zone."""
3import collections
4import json
5import sys
6import time
7import numpy as np
8
9from itertools import product
10from tabulate import tabulate
11from monty.json import MontyEncoder
12from monty.collections import AttrDict, dict2namedtuple
13from monty.functools import lazy_property
14from monty.string import marquee
15from pymatgen.core.lattice import Lattice
16from pymatgen.util.serialization import pmg_serialize
17from pymatgen.util.serialization import SlotPickleMixin
18from abipy.iotools import ETSF_Reader
19from abipy.tools.derivatives import finite_diff
20from abipy.tools.numtools import add_periodic_replicas, is_diagonal
21
22import logging
23logger = logging.getLogger(__name__)
24
25__all__ = [
26    "issamek",
27    "wrap_to_ws",
28    "wrap_to_bz",
29    "as_kpoints",
30    "Kpoint",
31    "KpointList",
32    "KpointStar",
33    "Kpath",
34    "IrredZone",
35    "rc_list",
36    "kmesh_from_mpdivs",
37    "Ktables",
38    "find_points_along_path",
39]
40
41# Tolerance used to compare k-points.
42_ATOL_KDIFF = 1e-8
43
44# Tolerances passed to spglib.
45_SPGLIB_SYMPREC = 1e-5
46_SPGLIB_ANGLE_TOLERANCE = -1.0
47
48
49def set_atol_kdiff(new_atol):
50    """
51    Change the value of the tolerance ``_ATOL_KDIFF`` used to compare k-points.
52    Return old value
53
54    .. warning::
55
56        This function should be called at the beginning of the script.
57    """
58    global _ATOL_KDIFF
59    old_atol = _ATOL_KDIFF
60    _ATOL_KDIFF = new_atol
61    return old_atol
62
63
64def set_spglib_tols(symprec, angle_tolerance):
65    """
66    Change the value of the tolerances ``symprec`` and ``angle_tolerance``
67    used to call spglib_. Return old values
68
69    .. warning::
70
71        This function should be called at the beginning of the script.
72    """
73    global _SPGLIB_SYMPREC, _SPGLIB_ANGLE_TOLERANCE
74    old_symprec, old_angle_tolerance = _SPGLIB_SYMPREC, _SPGLIB_ANGLE_TOLERANCE
75    _SPGLIB_SYMPREC, _SPGLIB_ANGLE_TOLERANCE = symprec, angle_tolerance
76    return old_symprec, old_angle_tolerance
77
78
79def is_integer(x, atol=None):
80    """
81    True if all x is integer within the absolute tolerance atol.
82    Use _ATOL_KDIFF is atol is None.
83
84    >>> assert is_integer([1., 2.])
85    >>> assert is_integer(1.01, atol=0.011)
86    >>> assert not is_integer([1.01, 2])
87    """
88    if atol is None: atol = _ATOL_KDIFF
89    int_x = np.around(x)
90    return np.allclose(int_x, x, atol=atol)
91
92
93def issamek(k1, k2, atol=None):
94    """
95    True if k1 and k2 are equal modulo a lattice vector.
96    Use _ATOL_KDIFF is atol is None.
97
98    >>> assert issamek([1, 1, 1], [0, 0, 0])
99    >>> assert issamek([1.1, 1, 1], [0, 0, 0], atol=0.1)
100    >>> assert not issamek(0.00003, 1)
101    """
102    k1 = np.asarray(k1)
103    k2 = np.asarray(k2)
104    #if k1.shape != k2.shape:
105
106    return is_integer(k1 - k2, atol=atol)
107
108
109def wrap_to_ws(x):
110    """
111    Transforms x in its corresponding reduced number in the interval ]-1/2,1/2].
112    """
113    w = x % 1
114    return np.where(w > 0.5, w - 1.0, w)
115
116
117def wrap_to_bz(x):
118    """
119    Transforms x in its corresponding reduced number in the interval [0,1[."
120    """
121    return x % 1
122
123
124def rc_list(mp, sh, pbc=False, order="bz"):
125    """
126    Returns a |numpy-array| with the linear mesh used to sample one dimension of the reciprocal space.
127    Note that the coordinates are always ordered so that rc[i+1] > rc[i].
128    so that we can easily plot quantities defined on the 3D multidimensional mesh.
129
130    Args:
131        mp: Number of Monkhorst-Pack divisions along this direction.
132        sh: Shift
133        pbc: if pbc is True, periodic boundary conditions are enforced.
134        order: Possible values ["bz", "unit_cell"].
135            if "bz", the coordinates are forced to be in [-1/2, 1/2)
136            if "unit_cell", the coordinates are forced to be in [0, 1).
137    """
138    rc = []
139
140    if order == "unit_cell":
141        n = mp if not pbc else mp + 1
142        for i in range(n):
143            rc.append((i + sh) / mp)
144
145    elif order == "bz":
146        for i in range(mp):
147            x = (i + sh) / mp
148
149            if x < 0.5:
150                rc.append(x)
151            else:
152                # Insert xm1 in rc so that we still have a ordered list.
153                xm1 = x - 1.0
154                for i, c in enumerate(rc):
155                    if c > xm1:
156                        break
157                else:
158                    raise ValueError()
159
160                rc.insert(i, xm1)
161
162        if pbc:
163            rc.append(rc[0] + 1.0)
164
165    else:
166        raise ValueError("Wrong order %s" % order)
167
168    return np.array(rc)
169
170
171def kmesh_from_mpdivs(mpdivs, shifts, pbc=False, order="bz"):
172    """
173    Returns a |numpy-array| with the reduced coordinates of the
174    k-points from the MP divisions and the shifts.
175
176    Args:
177        mpdivs: The three MP divisions.
178        shifts: Array-like object with the MP shift.
179        pbc: If True, periodic images of the k-points will be included i.e. closed mesh.
180        order: "unit_cell" if the kpoint coordinates must be in [0,1)
181               "bz" if the kpoint coordinates must be in [-1/2, +1/2)
182    """
183    shifts = np.reshape(shifts, (-1, 3))
184    assert np.all(np.abs(shifts) <= 0.5)
185
186    # Build k-point grid.
187    kbz = collections.deque()
188    for ish, shift in enumerate(shifts):
189        rc0 = rc_list(mpdivs[0], shift[0], pbc=pbc, order=order)
190        rc1 = rc_list(mpdivs[1], shift[1], pbc=pbc, order=order)
191        rc2 = rc_list(mpdivs[2], shift[2], pbc=pbc, order=order)
192
193        for kxyz in product(rc0, rc1, rc2):
194            kbz.append(kxyz)
195
196    return np.array(kbz)
197
198
199def map_grid2ibz(structure, ibz, ngkpt, has_timrev, pbc=False):
200    """
201    Compute the correspondence between a *grid* of k-points in the *unit cell*
202    associated to the ``ngkpt`` mesh and the corresponding points in the IBZ.
203    Requires structure with Abinit symmetries.
204    This routine is mainly used to symmetrize eigenvalues in the unit cell
205    e.g. to write BXSF files for electronic isosurfaces.
206
207    Args:
208        structure: Structure with (Abinit) symmetry operations.
209        ibz: [*, 3] array with reduced coordinates in the in the IBZ.
210        ngkpt: Mesh divisions.
211        has_timrev: True if time-reversal can be used.
212        pbc: True if the mesh should contain the periodic images (closed mesh).
213
214    Returns:
215        bz2ibz: 1d array with BZ --> IBZ mapping
216    """
217    ngkpt = np.asarray(ngkpt, dtype=int)
218
219    # Extract (FM) symmetry operations in reciprocal space.
220    abispg = structure.abi_spacegroup
221    if abispg is None:
222        raise ValueError("Structure does not contain Abinit spacegroup info!")
223
224    # Extract rotations in reciprocal space (FM part).
225    symrec_fm = [o.rot_g for o in abispg.fm_symmops]
226
227    # Compute TS k_ibz.
228    bzgrid2ibz = -np.ones(ngkpt, dtype=int)
229
230    for ik_ibz, kibz in enumerate(ibz):
231        gp_ibz = np.array(np.rint(kibz * ngkpt), dtype=int)
232        for rot in symrec_fm:
233            rot_gp = np.matmul(rot, gp_ibz)
234            gp_bz = rot_gp % ngkpt
235            bzgrid2ibz[gp_bz[0], gp_bz[1], gp_bz[2]] = ik_ibz
236            if has_timrev:
237                gp_bz = (-rot_gp) % ngkpt
238                bzgrid2ibz[gp_bz[0], gp_bz[1], gp_bz[2]] = ik_ibz
239
240    if pbc:
241        # Add periodic replicas.
242        bzgrid2ibz = add_periodic_replicas(bzgrid2ibz)
243
244    if np.any(bzgrid2ibz == -1):
245        #for ik_bz, ik_ibz in enumerate(self.bzgrid2ibz): print(ik_bz, ">>>", ik_ibz)
246        msg = "Found %s/%s invalid entries in bzgrid2ibz array" % ((bzgrid2ibz == -1).sum(), bzgrid2ibz.size)
247        msg += "This can happen if there an inconsistency between the input IBZ and ngkpt"
248        msg += "ngkpt: %s, has_timrev: %s" % (str(ngkpt), has_timrev)
249        raise ValueError(msg)
250
251    bz2ibz = bzgrid2ibz.flatten()
252    return bz2ibz
253
254    """
255    for ik_bz, kbz in enumerate(bz):
256        found = False
257        for ik_ibz, kibz in enumerate(ibz):
258            if found: break
259            for symmop in structure.spacegroup:
260                krot = symmop.rotate_k(kibz)
261                if issamek(krot, kbz):
262                    bz2ibz[ik_bz] = ik_ibz
263                    found = True
264                    break
265    """
266
267
268def has_timrev_from_kptopt(kptopt):
269    """
270    True if time-reversal symmetry can be used to generate k-points in the IBZ.
271    """
272    # note: We assume TR if negative value i.e. band structure k-sampling.
273    return int(kptopt) not in (3, 4)
274
275
276def kptopt2str(kptopt, verbose=0):
277    """
278    Return human-readable string with meaning of kptopt.
279    """
280    if kptopt < 0:
281        t = ("Band structure run. Use kptbounds, and ndivk (ndivsm)"
282             "The absolute value of kptopt gives the number of segments of the band structure." +
283             "Weights are usually irrelevant with this option")
284    else:
285        t = {
286            0: ("Manual mode",
287                "User-provided nkpt, kpt, kptnrm and wtk"),
288            1: ("Use space group symmetries and TR symmetry",
289                "Usual mode for GS calculations (ngkpt or kptrlatt, nshiftk and shiftk)"),
290            2: ("Only TR symmetry",
291                "This is to be used for DFPT at Gamma (ngkpt or kptrlatt, nshiftk and shiftk)"),
292            3: ("Do not take into account any symmetry",
293                "This is to be used for DFPT at non-zero q (ngkpt or kptrlatt, nshiftk and shiftk)."),
294            4: ("Spatial symmetries, NO TR symmetry",
295                "This has to be used for PAW calculations with SOC (pawspnorb/=0) " +
296                "from ngkpt or kptrlatt, nshiftk and shiftk."),
297        }[kptopt]
298
299    return t[0] if verbose == 0 else t[0] + "\n" + t[1]
300
301
302def map_kpoints(other_kpoints, other_lattice, ref_lattice, ref_kpoints, ref_symrecs, has_timrev):
303    """
304    Build mapping between a list of k-points in reduced coordinates (``other_kpoints``)
305    in the reciprocal lattice ``other_lattice`` and a list of reference k-points given
306    in the reciprocal lattice `ref_lattice` with symmetry operations ``ref_symrecs``.
307
308    Args:
309        other_kpoints:
310        other_lattice: matrix whose rows are the reciprocal lattice vectors in cartesian coordinates.
311        ref_lattice: same meaning as other_lattice.
312        ref_kpoints:
313        ref_symrecs: [nsym,3,3] arrays with symmetry operations in the `ref_lattice` reciprocal space.
314        has_timrev: True if time-reversal can be used.
315
316    Returns
317        (o2r_map, nmissing)
318
319        nmissing:
320            Number of k-points in ref_kpoints that cannot be mapped onto ref_kpoints.
321
322        o2r_map[i] gives the mapping  between the i-th k-point in other_kpoints and
323            ref_kpoints. Set to None if the i-th k-point does not have any image in ref.
324            Each entry is a named tuple with the following attributes:
325
326                ik_ref:
327                tsign:
328                isym
329                g0
330
331            kpt_other = TS kpt_ref + G0
332    """
333    ref_gprimd_inv = np.inv(np.asarray(ref_lattice).T)
334    other_gprimd = np.asarray(other_lattice).T
335    other_kpoints = np.asarray(other_kpoints).reshape((-1, 3))
336    ref_kpoints = np.asarray(ref_kpoints).reshape((-1, 3))
337    o2r_map = len(other_kpoints) * [None]
338
339    tsigns = (1, -1) if has_timrev else (1,)
340    kmap = collections.namedtuple("kmap", "ik_ref, tsign, isym, g0")
341
342    for ik_oth, okpt in enumerate(other_kpoints):
343        # Get other k-point in reduced coordinates in the reference lattice.
344        okpt_red = np.matmul(ref_gprimd_inv, np.matmul(other_gprimd, okpt))
345
346        # k_other = TS k_ref + G0
347        found = False
348        for ik_ref, kref in enumerate(ref_kpoints):
349            if found: break
350            for tsign in tsigns:
351                for isym, symrec in enumerate(ref_symrecs):
352                    krot = tsign * np.matmul(symrec, kref)
353                    if issamek(okpt_red, krot):
354                        g0 = np.rint(okpt_red - krot)
355                        o2r_map[ik_oth] = kmap(ik_ref, tsign, isym, g0)
356                        found = True
357                        break
358
359        return o2r_map, o2r_map.count(None)
360
361
362#def find_irred_kpoints_kmesh(structure, kfrac_coords):
363#    """
364#    Remove k-points that are connected to each other by one of the
365#    symmetry operations of the space group. Assume k-points
366#    belonging to a homogeneous mesh.
367#
368#    Args:
369#        structure: Structure object.
370#        kfrac_coords: Reduced coordinates of the k-points.
371#
372#    Return:
373#    """
374#    # Wrap in [0,1[ interval.
375#    uc_kcoords = np.reshape(kfrac_coords, (-1, 3)) % 1
376#    numk = len(uc_kcoords)
377#    nx, ny, nz = np.int(np.floor(1 / uc_kcoords.min(axis=0)))
378#
379#    # Compute rank and invrank
380#    rank = np.array(numk, dtype=np.int)
381#    invrank = {}
382#    for ik, kk in enumerate(uc_kcoords):
383#        rk = iz + iy * nz + ix * ny * nz
384#        rank[ik] = rk
385#        invrank[rank] = ik
386#
387#    irred_map = collections.deque()
388#    irred_map.append(0)
389#    kpts2irred = collections.deque()
390#    kpts2irred.append((0, 0, +1))
391#
392#    for ik, kk in enumerate(uc_kcoords[1:]):
393#        ik += 1
394#        found = False
395#        for ik_irr in irred_map:
396#            kirr = kfrac_coords[ik_irr]
397#            for isym, symmop in enumerate(structure.abi_spacegroup):
398#                krot = symmop.rotate_k(kirr)
399#                new_frac_coords = krot.frac_coords % 1
400#                if issamek(krot, kk):
401#                    #kpts2irred[ik] = ik_irr
402#                    #kpts2irred[ik] = isym
403#                    found = True
404#                    break
405#
406#    #return irred_map
407
408
409def find_irred_kpoints_generic(structure, kfrac_coords, verbose=1):
410    """
411    Remove the k-points that are connected to each other by one of the
412    symmetry operations of the space group. No assumption is done
413    on the initial k-point sampling, this means that one can call this
414    function to treat points on a path in reciprocal space.
415
416    Args:
417        structure: |Structure| object.
418        kfrac_coords: Reduced coordinates of the k-points.
419
420    Return:
421        irred_map: Index of the i-th irreducible k-point in the input kfrac_coords array.
422
423    .. warning::
424
425        In the worst case, the algorithm scales as nkpt ** 2 * nsym.
426        hence this routine should be used only if ``kfrac_coords`` represents
427        e.g. a path in the Brillouin zone or an arbitrary set of points.
428    """
429    start = time.time()
430    print("Removing redundant k-points. This is gonna take a while... ")
431
432    # Wrap points in [0,1[ interval.
433    uc_kcoords = np.reshape(kfrac_coords, (-1, 3)) % 1
434
435    irred_map = collections.deque()
436    irred_map.append(0)
437    kpts2irred = collections.deque()
438    kpts2irred.append((0, 0, +1))
439
440    for ik, kk in enumerate(uc_kcoords[1:]):
441        ik += 1
442        found = False
443        for ik_irr in irred_map:
444            kirr = kfrac_coords[ik_irr]
445            for isym, symmop in enumerate(structure.abi_spacegroup):
446                krot = symmop.rotate_k(kirr)
447                if issamek(krot, kk):
448                    found = True
449                    #kpts2irred[ik] = (ik_irr, isym, symmops.time_sign)
450                    break
451
452        if not found:
453            irred_map.append(ik)
454
455    print("Completed in", time.time() - start, "[s]")
456    if verbose:
457        print("Entered with ", len(uc_kcoords), "k-points")
458        print("Found ", len(irred_map), "irred k-points")
459
460    return dict2namedtuple(irred_map=np.array(irred_map, dtype=int))
461
462
463def kpath_from_bounds_and_ndivsm(bounds, ndivsm, structure):
464    """
465    Generate a normalized path given the extrema and the number of divisions for the smallest segment
466
467    Args:
468        bounds: (N, 3) array with the boundaries of the path in reduced coordinates.
469        ndivsm: Number of divisions used for the smallest segment.
470
471    Return:
472        Array (M, 3) with fractional coordinates.
473    """
474    bounds = np.reshape(bounds, (-1, 3))
475    nbounds = len(bounds)
476    if nbounds == 1:
477        raise ValueError("Need at least two points to define the k-path!")
478
479    lens = []
480    for i in range(nbounds - 1):
481        v = bounds[i + 1] - bounds[i]
482        lens.append(float(structure.reciprocal_lattice.norm(v)))
483
484    # Avoid division by zero if any bounds[i+1] == bounds[i]
485    minlen = np.min(lens)
486    if minlen < 1e-6:
487        raise ValueError("Found two equivalent consecutive points in bounds!")
488
489    minlen = minlen / ndivsm
490    ndivs = np.rint(lens / minlen).astype(int)
491    path = []
492    for i in range(nbounds - 1):
493        for j in range(ndivs[i]):
494            p = bounds[i] + j * (bounds[i + 1] - bounds[i]) / ndivs[i]
495            path.append(p)
496    path.append(bounds[-1])
497
498    return np.array(path)
499
500
501class KpointsError(Exception):
502    """Base error class for KpointList exceptions."""
503
504
505def as_kpoints(obj, lattice, weights=None, names=None):
506    """
507    Convert obj into a list of k-points.
508
509    Args:
510        obj: :class:`Kpoint` or list of Kpoint objects or array-like object.
511        lattice: Reciprocal lattice.
512        weights: k-point weights. Ignored if obj is already a `Kpoint` instance or a list of `Kpoint` items.
513        name: string with the name of the k-point. Ignored if obj is already a `Kpoint`
514              instance or a list of `Kpoint` items.
515    """
516    # K-point?
517    if isinstance(obj, Kpoint):
518        return [obj]
519
520    # Iterable with K-points?
521    if isinstance(obj, collections.abc.Iterable):
522        if isinstance(obj[0], Kpoint):
523            assert all(isinstance(o, Kpoint) for o in obj)
524            return obj
525
526    # Assume array-like
527    obj = np.reshape(np.asarray(obj), (-1, 3))
528    ndim = obj.ndim
529
530    if ndim == 1:
531        return [Kpoint(obj, lattice, weight=weights, name=names)]
532
533    elif ndim == 2:
534        nk = len(obj)
535        if weights is None: weights = nk * [None]
536        if names is None: names = nk * [None]
537        return [Kpoint(rc, lattice, weight=w, name=l) for (rc, w, l) in zip(obj, weights, names)]
538
539    else:
540        raise ValueError("ndim > 2 is not supported")
541
542
543class Kpoint(SlotPickleMixin):
544    """
545    Class defining one k-point. This object is immutable and can be used as key in dictionaries
546
547    Note that we usually construct the object by passing pymatgen.reciprocal_lattice
548    that is the standard reciprocal lattice used for solid state physics
549    with a factor of 2 * pi i.e. a_i . b_j = 2pi delta_ij.
550    Abinit, on the contrary, uses the crystallographic reciprocal lattice i.e. no 2pi factor.
551    so pay attention when converting Abinit routines to AbiPy.
552    """
553
554    __slots__ = [
555        "_frac_coords",
556        "_lattice",
557        "_weight",
558        "_name",
559        "_hash",
560    ]
561
562    @classmethod
563    def from_name_and_structure(cls, name, structure):
564        """
565        Build Kpoint object from string with name and structure.
566        """
567        frac_coords = structure.get_kcoords_from_names(name)
568        frac_coords = np.reshape(frac_coords, (3,))
569        return cls(frac_coords, structure.reciprocal_lattice, weight=None, name=name)
570
571    def __init__(self, frac_coords, lattice, weight=None, name=None):
572        """
573        Args:
574            frac_coords: Reduced coordinates of the k-point.
575            lattice: |Lattice| object describing the reciprocal lattice.
576            weights: k-point weight (optional, set to zero if not given).
577            name: string with the name of the k-point (optional)
578        """
579        self._frac_coords = np.asarray(frac_coords)
580        if len(self.frac_coords) != 3:
581            raise TypeError("Expecting vector with 3 items, got `%s`" % str(self.frac_coords))
582
583        self._lattice = lattice
584        self.set_weight(weight)
585        self.set_name(name)
586
587    #def __array__(self, **kwargs):
588    #    """np.array(self)"""
589    #    print(kwargs)
590    #    dtype = kwargs.pop("dtype", None)
591    #    if dtype is None:
592    #        return self._frac_coords
593    #    else:
594    #        return np.array(self._frac_coords, dtype=dtype)
595
596    def __hash__(self):
597        """
598        Kpoint objects can be used as keys in dictionaries.
599
600        .. warning::
601
602            The hash is computed from the fractional coordinates (floats).
603            Hence one should avoid using hashes for implementing search algorithms
604            in which new Kpoints are, for example generated by means of
605            symmetry operations. This means that a dict of Kpoint objects
606            is safe to use only when we are sure than we are going to access
607            its entries with the *same* keys used to generate the dict!.
608        """
609        try:
610            return self._hash
611        except AttributeError:
612            self._hash = hash(tuple(wrap_to_ws(self.frac_coords)))
613            return self._hash
614
615    @property
616    def frac_coords(self):
617        """Fractional coordinates of the k-points."""
618        return self._frac_coords
619
620    @property
621    def lattice(self):
622        """|Lattice| object describing the Reciprocal lattice."""
623        return self._lattice
624
625    @property
626    def weight(self):
627        """Weight of the k-point. 0.0 if the weight is not defined."""
628        if self._weight is None:
629            return 0.0
630        else:
631            return self._weight
632
633    def set_weight(self, weight):
634        """Set the weight of the k-point."""
635        self._weight = weight
636
637    @lazy_property
638    def cart_coords(self):
639        """Cartesian coordinates of the k-point."""
640        return self.lattice.get_cartesian_coords(self.frac_coords)
641
642    @property
643    def name(self):
644        """Name of the k-point. None if not available."""
645        return self._name
646
647    def set_name(self, name):
648        """Set the name of the k-point."""
649        # Fix typo in Latex syntax (if any).
650        if name is not None and name.startswith("\\"): name = "$" + name + "$"
651        self._name = name
652
653    @lazy_property
654    def on_border(self):
655        """
656        True if the k-point is on the border of the BZ (lattice translations are taken into account).
657        """
658        kreds = wrap_to_ws(self.frac_coords)
659        diff = np.abs(np.abs(kreds) - 0.5)
660        return np.any(diff < _ATOL_KDIFF)
661
662    def __repr__(self):
663        s = "[%+.3f, %+.3f, %+.3f]" % tuple(self.frac_coords)
664        if self.name is not None:
665            s += " %s" % self.name
666        return s
667
668    def tos(self, m="fract"):
669        """
670        Return string with fractional or cartesian coords depending
671        on mode `m` in ("fract", "cart", "fracart")
672        """
673        if m == "fract":
674            return "[%+.3f, %+.3f, %+.3f]" % tuple(self.frac_coords)
675        elif m == "cart":
676            return "(%+.3f, %+.3f, %+.3f)" % tuple(self.cart_coords)
677        elif m == "fracart":
678            return "%s, %s" % (self.tos(m="fract"), self.tos(m="cart"))
679        else:
680            raise ValueError("Invalid mode: `%s`" % str(m))
681
682    def __str__(self):
683        return self.to_string()
684
685    def to_string(self, verbose=0):
686        """String representation."""
687        s = "[%+.3f, %+.3f, %+.3f]" % tuple(self.frac_coords)
688        if self.name is not None:
689            s += ", name: %s" % self.name
690        if self._weight is not None: s += ", weight: %.3f" % self.weight
691        return s
692
693    # Kpoint algebra.
694    def __add__(self, other):
695        return self.__class__(self.frac_coords + other.frac_coords, self.lattice)
696
697    def __sub__(self, other):
698        return self.__class__(self.frac_coords - other.frac_coords, self.lattice)
699
700    def __eq__(self, other):
701        if hasattr(other, "frac_coords"):
702            # Comparison between two Kpoint objects
703            return issamek(self.frac_coords, other.frac_coords)
704        else:
705            # Kpoint vs iterable (e.g. list)
706            return issamek(self.frac_coords, other)
707
708    def __ne__(self, other):
709        return not (self == other)
710
711    def __getitem__(self, slice):
712        return self.frac_coords[slice]
713
714    @classmethod
715    def as_kpoint(cls, obj, lattice):
716        """
717        Convert obj into a :class:`Kpoint` instance.
718
719        Args:
720            obj: :class:`Kpoint` instance or array-like with the reduced coordinates.
721            lattice: |Lattice| object defining the reciprocal lattice.
722        """
723        if isinstance(obj, cls):
724            return obj
725        else:
726            return cls(obj, lattice, weight=None, name=None)
727
728    @classmethod
729    def gamma(cls, lattice, weight=None):
730        """Constructor for the Gamma point."""
731        return cls(np.zeros(3), lattice, weight=weight, name=r"$\Gamma$")
732
733    def copy(self):
734        """Deep copy."""
735        return self.__class__(self.frac_coords.copy(), self.lattice.copy(),
736                              weight=self.weight, name=self.name)
737
738    def is_gamma(self, allow_umklapp=False, atol=None):
739        """
740        Return True if this the gamma point.
741
742        Args:
743            allow_umklapp: True if umklapp G-vectors are allowed.
744            atol: Absolute tolerance for k-point comparison (used only if umklapp).
745        """
746        if not allow_umklapp:
747            return np.all(self.frac_coords == 0.0)
748        else:
749            return issamek(self.frac_coords, [0, 0, 0], atol=atol)
750
751    @lazy_property
752    def norm(self):
753        """Norm of the kpoint."""
754        return np.sqrt(np.dot(self.cart_coords, self.cart_coords))
755
756    def versor(self):
757        """Returns the versor i.e. math:`||k|| = 1`"""
758        cls = self.__class__
759        if self.norm > 1e-12:
760            return cls(self.frac_coords / self.norm, self.lattice, weight=self.weight)
761        else:
762            return cls.gamma(self.lattice, weight=self.weight)
763
764    def wrap_to_ws(self):
765        """Returns a new |Kpoint| in the Wigner-Seitz zone."""
766        return self.__class__(wrap_to_ws(self.frac_coords), self.lattice,
767                              name=self.name, weight=self.weight)
768
769    def wrap_to_bz(self):
770        """Returns a new |Kpoint| in the first unit cell."""
771        return self.__class__(wrap_to_bz(self.frac_coords), self.lattice,
772                              name=self.name, weight=self.weight)
773
774    def compute_star(self, symmops, wrap_tows=True):
775        """Return the star of the kpoint (tuple of |Kpoint| objects)."""
776        frac_coords = [self.frac_coords]
777
778        # TODO: This part becomes a bottleneck for large nk!
779        for sym in symmops:
780            sk_coords = sym.rotate_k(self.frac_coords, wrap_tows=wrap_tows)
781
782            # Add it only if it's not already in the list.
783            for prev_coords in frac_coords:
784                if issamek(sk_coords, prev_coords): break
785            else:
786                frac_coords.append(sk_coords)
787
788        return KpointStar(self.lattice, frac_coords, weights=None, names=len(frac_coords) * [self.name])
789
790
791class KpointList(collections.abc.Sequence):
792    """
793    Base class defining a sequence of |Kpoint| objects. Essentially consists
794    of base methods implementing the sequence protocol and helper functions.
795    The subclasses |Kpath| and |IrredZone| provide specialized methods to operate
796    on k-points representing a path or list of points in the IBZ, respectively.
797    This object is immutable.
798
799    .. Note:
800
801        Algorithms usually need to know what kind of sampling we are using.
802        The test can be easily implemented with:
803
804        if kpoints.is_path:
805            # code specific to k-paths.
806
807        elif kpoints.is_ibz:
808            # code specific to IBZ sampling.
809
810    .. rubric:: Inheritance Diagram
811    .. inheritance-diagram:: KpointList
812    """
813    Error = KpointsError
814
815    @classmethod
816    def subclass_from_name(cls, name):
817        """Return the class with the given name."""
818        if cls.__name__ == name: return cls
819        for c in cls.__subclasses__():
820            if c.__name__ == name: return c
821
822        raise ValueError("Cannot find subclass associated to name: %s" % name)
823
824    @classmethod
825    def from_dict(cls, d):
826        """
827        Makes Kpoints obey the general json interface used in pymatgen for easier serialization.
828        """
829        reciprocal_lattice = Lattice.from_dict(d["reciprocal_lattice"])
830        return cls(reciprocal_lattice, d["frac_coords"],
831                   weights=d["weights"], names=d["names"], ksampling=d["ksampling"])
832
833    @pmg_serialize
834    def as_dict(self):
835        """
836        Makes Kpoints obey the general json interface used in pymatgen for easier serialization.
837        """
838        if self.weights is not None: weights = self.weights.tolist()
839        return dict(
840            reciprocal_lattice=self.reciprocal_lattice.as_dict(),
841            frac_coords=self.frac_coords.tolist(),
842            weights=weights,
843            names=[k.name for k in self],
844            ksampling=self.ksampling,
845        )
846
847    def __init__(self, reciprocal_lattice, frac_coords, weights=None, names=None, ksampling=None):
848        """
849        Args:
850            reciprocal_lattice: |Lattice| object.
851            frac_coords: Array-like object with the reduced coordinates of the k-points.
852            weights: List of k-point weights. If None, weights are initialized with zeros.
853            names: List of k-point names.
854            ksampling: Info on the k-point sampling (used for homogeneous meshes)
855        """
856        self._reciprocal_lattice = reciprocal_lattice
857        self._frac_coords = frac_coords = np.reshape(frac_coords, (-1, 3))
858        self.ksampling = ksampling
859
860        if weights is not None:
861            if len(weights) != len(frac_coords):
862                raise ValueError("len(weights) != len(frac_coords):\nweights: %s\nfrac_coords: %s" %
863                    (weights, frac_coords))
864            weights = np.asarray(weights)
865        else:
866            weights = np.zeros(len(self.frac_coords))
867
868        if names is not None and len(names) != len(frac_coords):
869            raise ValueError("len(names) != len(frac_coords):\nnames: %s\nfrac_coords: %s" %
870                    (names, frac_coords))
871
872        self._points = []
873        for i, rcs in enumerate(frac_coords):
874            name = None if names is None else names[i]
875            self._points.append(Kpoint(rcs, self.reciprocal_lattice, weight=weights[i], name=name))
876
877    @property
878    def reciprocal_lattice(self):
879        """|Lattice| object defining the reciprocal lattice."""
880        return self._reciprocal_lattice
881
882    def __repr__(self):
883        return self.to_string(func=repr)
884
885    def __str__(self):
886        return self.to_string(func=str)
887
888    def to_string(self, func=str, title=None, verbose=0):
889        """String representation."""
890        lines = []; app = lines.append
891        if title is not None: app(marquee(title, mark="="))
892        lines.extend(["%d) %s" % (i, func(kpoint)) for i, kpoint in enumerate(self)])
893
894        return "\n".join(lines)
895
896    # Sequence protocol.
897    def __len__(self):
898        return len(self._points)
899
900    def __iter__(self):
901        return self._points.__iter__()
902
903    def __getitem__(self, slice):
904        return self._points[slice]
905
906    def __contains__(self, kpoint):
907        return kpoint in self._points
908
909    def __reversed__(self):
910        return self._points.__reversed__()
911
912    def __add__(self, other):
913        if self.reciprocal_lattice != other.reciprocal_lattice:
914            raise ValueError("Cannot merge k-points with different reciprocal lattice.")
915
916        return KpointList(self.reciprocal_lattice,
917                          frac_coords=[k.frac_coords for k in self] + [k.frac_coords for k in other],
918                          weights=None,
919                          names=[k.name for k in self] + [k.name for k in other],
920                        )
921
922    def __eq__(self, other):
923        if other is None or not isinstance(other, KpointList): return False
924        for k1, k2 in zip(self, other):
925            if k1 != k2: return False
926        return True
927
928    def __ne__(self, other):
929        return not (self == other)
930
931    def index(self, kpoint):
932        """
933        Returns: the first index of kpoint in self.
934
935        Raises: `ValueError` if not found.
936        """
937        try:
938            return self._points.index(kpoint)
939        except ValueError:
940            raise ValueError("Cannot find point: %s in KpointList:\n%s" % (repr(kpoint), repr(self)))
941
942    def get_all_kindices(self, kpoint):
943        """
944        Return numpy array with indexes of all the k-point
945        Accepts: |Kpoint| instance or integer.
946        """
947        start = self.index(kpoint)
948        k0 = self[start]
949        kinds = []
950        for ik, k in enumerate(self):
951            if k == k0: kinds.append(ik)
952        return np.array(kinds)
953
954    def find(self, kpoint):
955        """
956        Returns: first index of kpoint. -1 if not found
957        """
958        try:
959            return self.index(kpoint)
960        except ValueError:
961            return -1
962
963    def count(self, kpoint):
964        """Return number of occurrences of kpoint"""
965        return self._points.count(kpoint)
966
967    def find_closest(self, obj):
968        """
969        Find the closest k-point in the list (not necessarily equal).
970
971        Args:
972            obj: Fractional coordinates or |Kpoint| instance.
973
974        Return:
975            (ind, kpoint, dist)
976
977            where ``ind`` is the index in self of the closest k-point.
978            ``kpoint`` is the |Kpoint| instance of index ``ind``.
979            dist is the distance between ``obj`` and ``kpoint``.
980        """
981        if isinstance(obj, Kpoint):
982            if obj.lattice != self.reciprocal_lattice:
983                raise ValueError("Kpoint list and Kpoint object have different lattices!")
984            frac_coords = obj.frac_coords
985        else:
986            frac_coords = np.asarray(obj)
987
988        dist = np.empty(len(self))
989        for i, kpt in enumerate(self):
990            dist[i] = kpt.lattice.norm(kpt.frac_coords - frac_coords)
991
992        ind = dist.argmin()
993        return ind, self[ind], np.copy(dist[ind])
994
995    @property
996    def is_path(self):
997        """True if self represents a path in the BZ."""
998        return isinstance(self, Kpath)
999
1000    @property
1001    def is_ibz(self):
1002        """True if self represents a list of points in the IBZ."""
1003        return isinstance(self, IrredZone)
1004
1005    @lazy_property
1006    def mpdivs_shifts(self):
1007        """
1008        The Monkhorst-Pack (MP) divisions and shifts.
1009        Both quantities are set to None if self is not a MP mesh.
1010        Use `is_mpmesh` to check whether self is a MP mesh.
1011        """
1012        if not self.is_ibz: return (None, None)
1013        # Test if kptrlatt is diagonal.
1014        if not is_diagonal(self.ksampling.kptrlatt): return (None, None)
1015        return self.ksampling.kptrlatt.diagonal(), self.ksampling.shifts
1016
1017    @property
1018    def is_mpmesh(self):
1019        """
1020        True if self represents a Monkhorst-Pack mesh.
1021        i.e if the sampling has been specified in terms of divisions
1022        along the reciprocal lattice vectors (ngkpt)
1023        """
1024        return self.mpdivs_shifts[0] is not None
1025
1026    @property
1027    def frac_coords(self):
1028        """Fractional coordinates of the k-point as |numpy-array| of shape (len(self), 3)"""
1029        return self._frac_coords
1030
1031    def get_cart_coords(self):
1032        """Cartesian coordinates of the k-point as |numpy-array| of shape (len(self), 3)"""
1033        return np.reshape([k.cart_coords for k in self], (-1, 3))
1034
1035    @property
1036    def names(self):
1037        """List with the name of the k-points."""
1038        return [k.name for k in self]
1039
1040    @property
1041    def weights(self):
1042        """|numpy-array| with the weights of the k-points."""
1043        return np.array([kpoint.weight for kpoint in self])
1044
1045    def sum_weights(self):
1046        """Returns the sum of the weights."""
1047        return np.sum(self.weights)
1048
1049    def check_weights(self):
1050        """
1051        Check if weights are normalized to one.
1052        Raises: `ValueError` if Weights are not normalized.
1053        """
1054        # Weights must be normalized to one.
1055        wsum = self.sum_weights()
1056        if abs(wsum - 1) > 1.e-6:
1057            err_msg = "Kpoint weights should sum up to one while sum_weights is %.3f\n" % wsum
1058            err_msg += "The list of kpoints does not represent a homogeneous sampling of the BZ\n"
1059            err_msg += "%s\n%s" % (self.__class__, self.to_string(verbose=0))
1060            raise ValueError(err_msg)
1061
1062    def remove_duplicated(self):
1063        """
1064        Remove duplicated k-points from self. Returns new :class:`KpointList` instance.
1065        """
1066        frac_coords, good_indices = [self[0].frac_coords], [0]
1067
1068        for i, kpoint in enumerate(self[1:]):
1069            i += 1
1070            # Add it only if it's not already in the list.
1071            for prev_coords in frac_coords:
1072                if issamek(kpoint.frac_coords, prev_coords): break
1073            else:
1074                frac_coords.append(kpoint.frac_coords)
1075                good_indices.append(i)
1076
1077        good_kpoints = [self[i] for i in good_indices]
1078
1079        return self.__class__(
1080                self.reciprocal_lattice,
1081                frac_coords=[k.frac_coords for k in good_kpoints],
1082                weights=None,
1083                names=[k.name for k in good_kpoints],
1084                ksampling=self.ksampling)
1085
1086    def to_array(self):
1087        """Returns a |numpy-array| [nkpy, 3] with the fractional coordinates."""
1088        return np.array(self.frac_coords.copy())
1089
1090    def to_json(self):
1091        """
1092        Returns a JSON_ string representation of the MSONable object.
1093        """
1094        return json.dumps(self.as_dict(), cls=MontyEncoder)
1095
1096    def plot(self, ax=None, **kwargs):
1097        """Plot k-points with matplotlib."""
1098        from pymatgen.electronic_structure.plotter import plot_brillouin_zone
1099        fold = False
1100        if self.is_path:
1101            labels = {k.name: k.frac_coords for k in self if k.name}
1102            frac_coords_lines = [self.frac_coords[line] for line in self.lines]
1103            return plot_brillouin_zone(self.reciprocal_lattice, lines=frac_coords_lines, labels=labels,
1104                                       ax=ax, fold=fold, **kwargs)
1105        else:
1106            # Not sure this works, I got points outside of the BZ in a simple with Si and Gamma-centered 8x8x8.
1107            # Don't know if it's a bug in matplotlib or plot_brillouin_zone.
1108            #print(self.frac_coords)
1109            return plot_brillouin_zone(self.reciprocal_lattice, kpoints=self.frac_coords,
1110                                       ax=ax, fold=fold, **kwargs)
1111
1112    def get_k2kqg_map(self, qpt, atol_kdiff=None):
1113        """
1114        Compute mapping k_index --> (k + q)_index, g0
1115
1116        Args:
1117            qpt: q-point in fractional coordinate or :class:`Kpoint` instance.
1118            atol_kdiff: Tolerance used to compare k-points.
1119                Use _ATOL_KDIFF is atol is None.
1120        """
1121        if atol_kdiff is None: atol_kdiff = _ATOL_KDIFF
1122        if isinstance(qpt, Kpoint):
1123            qfrac_coords = qpt.frac_coords
1124        else:
1125            qfrac_coords = np.reshape(qpt, (3,))
1126
1127        k2kqg = collections.OrderedDict()
1128        if np.all(np.abs(qfrac_coords) <= 1e-6):
1129            # Gamma point, DOH!
1130            g0 = np.zeros(3, dtype=int)
1131            for ik, _ in enumerate(self):
1132                k2kqg[ik] = (ik, g0)
1133        else:
1134            # N**2 scaling but this algorithm can handle k-paths
1135            # Note that in principle one could have multiple k+q in k-points
1136            # but only the first match is considered.
1137            for ik, kpoint in enumerate(self):
1138                kpq = kpoint.frac_coords + qfrac_coords
1139                for ikq, ksearch in enumerate(self):
1140                    if issamek(kpq, ksearch.frac_coords, atol=atol_kdiff):
1141                        g0 = np.rint(kpq - ksearch.frac_coords)
1142                        k2kqg[ik] = (ikq, g0)
1143                        break
1144
1145        return k2kqg
1146
1147
1148class KpointStar(KpointList):
1149    """
1150    Star of the kpoint. Note that the first k-point is assumed to be the base
1151    of the star namely the point that is used to generate the Star.
1152
1153    .. inheritance-diagram:: KpointStar
1154    """
1155    @property
1156    def base_point(self):
1157        """The point used to generate the star."""
1158        return self[0]
1159
1160    @property
1161    def name(self):
1162        """The name of the star (inherited from the name of the base point)."""
1163        return self.base_point.name
1164
1165
1166class Kpath(KpointList):
1167    """
1168    This object describes a k-path in reciprocal space.
1169    It provides methods to compute (line) derivatives along the path.
1170    The k-points do not have weights so Kpath should not be used to compute integral in the BZ.
1171
1172    .. inheritance-diagram:: Kpath
1173    """
1174
1175    @classmethod
1176    def from_names(cls, structure, knames, line_density=20):
1177        """
1178        Generate normalized K-path from list of k-point labels.
1179
1180        Args:
1181            structure: |Structure| object.
1182            knames: List of strings with the k-point labels.
1183            line_density: Number of points used to sample the smallest segment of the path
1184        """
1185        kfrac_coords = structure.get_kcoords_from_names(knames)
1186        vertices_names = list(zip(kfrac_coords, knames))
1187
1188        return cls.from_vertices_and_names(structure, vertices_names, line_density=line_density)
1189
1190    @classmethod
1191    def from_vertices_and_names(cls, structure, vertices_names, line_density=20):
1192        """
1193        Generate normalized K-path from a list of vertices and the corresponding labels.
1194
1195        Args:
1196            structure: |Structure| object.
1197            vertices_names:  List of tuple, each tuple is of the form (kfrac_coords, kname) where
1198                kfrac_coords are the reduced coordinates of the k-point and kname is a string with the name of
1199                the k-point. Each point represents a vertex of the k-path.
1200            line_density: Number of points used to sample the smallest segment of the path
1201        """
1202        gmet = structure.lattice.reciprocal_lattice.metric_tensor
1203        vnames = [str(vn[1]) for vn in vertices_names]
1204        vertices = np.array([vn[0] for vn in vertices_names], dtype=float)
1205        vertices.shape = (-1, 3)
1206
1207        dl_vals = []
1208        for ik, k0 in enumerate(vertices[:-1]):
1209            dk = vertices[ik + 1] - k0
1210            dl = np.sqrt(np.dot(dk, np.matmul(gmet, dk)))
1211            if abs(dl) < 1e-6: dl = np.inf
1212            dl_vals.append(dl)
1213
1214        dl_min = np.array(dl_vals).min()
1215
1216        fact = dl_min / line_density
1217        frac_coords = collections.deque()
1218        knames = collections.deque()
1219        for ik, dl in enumerate(dl_vals):
1220            if dl == np.inf: continue
1221            numk = int(np.rint(dl / fact))
1222            k0 = vertices[ik]
1223            dk = vertices[ik + 1] - k0
1224            knames.append(vnames[ik])
1225            for ii in range(numk):
1226                next_k = k0 + dk * ii / numk
1227                frac_coords.append(next_k)
1228                if ii > 0: knames.append("")
1229
1230        knames.append(vnames[-1])
1231        frac_coords.append(vertices[-1])
1232
1233        return cls(structure.lattice.reciprocal_lattice, frac_coords=frac_coords,
1234                   weights=None, names=knames)
1235
1236    def __str__(self):
1237        return self.to_string()
1238
1239    def to_string(self, verbose=0, title=None, **kwargs):
1240        """
1241        String representation.
1242
1243        Args:
1244            verbose: Verbosity level. Default: 0
1245        """
1246        lines = []; app = lines.append
1247        if title is not None: app(marquee(title, mark="="))
1248        app("K-path contains %s lines. Number of k-points in each line: %s" % (
1249            len(self.lines), [len(l) for l in self.lines]))
1250        if verbose:
1251            for i, line in enumerate(self.lines):
1252                app("line %d: %s" % (i, line))
1253        header = "\n".join(lines)
1254
1255        vids = {line[0] for line in self.lines}
1256
1257        table = [["Idx", "Frac_coords", "Name", "ds", "Vert",]]
1258        for i, kpoint in enumerate(self):
1259            tag = "*" if i in vids else " "
1260            if verbose == 0 and not tag: continue
1261            table.append([
1262                str(i),
1263                "%.7f, %.7f, %.7f" % tuple(kpoint.frac_coords),
1264                kpoint.name,
1265                self.ds[i] if i != len(self) - 1 else None,
1266                "*" if i in vids else " ",
1267            ])
1268
1269        return "\n".join([header, " ", tabulate(table, headers="firstrow")])
1270
1271    @lazy_property
1272    def ds(self):
1273        """
1274        |numpy-array| of len(self)-1 elements giving the distance between two
1275        consecutive k-points, i.e. ds[i] = ||k[i+1] - k[i]|| for i=0,1,...,n-1
1276        """
1277        ds = np.zeros(len(self) - 1)
1278        for i, kpoint in enumerate(self[:-1]):
1279            ds[i] = (self[i + 1] - kpoint).norm
1280        return ds
1281
1282    @lazy_property
1283    def versors(self):
1284        """
1285        Tuple of len(self) - 1 elements with the versors connecting k[i] to k[i+1].
1286        """
1287        versors = (len(self) - 1) * [None, ]
1288        for i, kpt in enumerate(self[:-1]):
1289            versors[i] = (self[i + 1] - kpt).versor()
1290        return tuple(versors)
1291
1292    @lazy_property
1293    def lines(self):
1294        """
1295        Nested list containing the indices of the points belonging to the same line.
1296        Used for extracting the eigenvalues while looping over the lines.
1297
1298        Example:
1299
1300            for line in self.lines:
1301                vals_on_line = eigens[spin, line, band]
1302        """
1303        prev = self.versors[0]
1304        lines = [[0]]
1305
1306        for i, v in enumerate(self.versors[1:]):
1307            i += 1
1308            #if v != prev:
1309            if ((prev - v).norm > 1e-5):
1310                #print("diff", (prev - v).norm, v.frac_coords - prev.frac_coords)
1311                prev = v
1312                lines[-1].append(i)
1313                lines.append([i])
1314            else:
1315                lines[-1].append(i)
1316
1317        lines[-1].append(len(self)-1)
1318        return tuple(lines)
1319
1320    @lazy_property
1321    def frac_bounds(self):
1322        """Numpy array of shape [M, 3] with the vertexes of the path in frac coords."""
1323        frac_bounds = [self[line[0]].frac_coords for line in self.lines]
1324        frac_bounds.append(self[self.lines[-1][-1]].frac_coords)
1325        return np.reshape(frac_bounds, (-1, 3))
1326
1327    @lazy_property
1328    def cart_bounds(self):
1329        """Numpy array of shape [M, 3] with the vertexes of the path in frac coords."""
1330        cart_bounds = [self[line[0]].cart_coords for line in self.lines]
1331        cart_bounds.append(self[self.lines[-1][-1]].cart_coords)
1332        return np.reshape(cart_bounds, (-1, 3))
1333
1334    def find_points_along_path(self, cart_coords, dist_tol=1e-12):
1335        """
1336        Find points in ``cart_coords`` lying on the path with distance less than `dist_tol`.
1337        See find_points_along_path function for API.
1338        """
1339        return find_points_along_path(self.cart_bounds, cart_coords, dist_tol=dist_tol)
1340
1341    def finite_diff(self, values, order=1, acc=4):
1342        """
1343        Compute the derivatives of values by finite differences.
1344
1345        Args:
1346            values: array-like object with shape=(nkpt) containing the values of the path.
1347            order: Order of the derivative.
1348            acc: Accuracy: 4 corresponds to a central difference with 5 points.
1349
1350        Returns:
1351            ragged numpy array. The i-th entry is a numpy array with the derivatives on the i-th line.
1352        """
1353        values = np.asarray(values)
1354        assert len(values) == len(self)
1355
1356        # Loop over the lines of the path, extract the data on the line and
1357        # differentiate f(s) where s is the distance between two consecutive points along the line.
1358        ders_on_lines = []
1359        for line in self.lines:
1360            vals_on_line = values[line]
1361            h = self.ds[line[0]]
1362            if not np.allclose(h, self.ds[line[:-1]]):
1363                raise ValueError("For finite difference derivatives, the path must be homogeneous!\n" +
1364                                 str(self.ds[line[:-1]]))
1365
1366            der = finite_diff(vals_on_line, h, order=order, acc=acc)
1367            ders_on_lines.append(der)
1368
1369        return np.array(ders_on_lines)
1370
1371
1372class IrredZone(KpointList):
1373    """
1374    An :class:`IrredZone` is a (immutable) sequence of points in the irreducible wedge of the BZ.
1375    Each point has a weight whose sum must equal 1 so that we can integrate quantities in the full Brillouin zone.
1376
1377    .. note::
1378
1379            Abinit supports different options for the specification of the BZ sampling:
1380
1381                 - kptrlatt(3,3) or ngkpt(3) for the definition grid.
1382                 - shiftk(3, nshiftk) for the definition of multiple shifts.
1383                 - `kptopt` for the treatment of symmetry operations.
1384
1385            All these possibilities complicate the internal implementation in particular when
1386            we need to recostruct the full BZ and take into account the presence of multiple shifts
1387            since kptrlatt may have non-zero off-diagonal components. Client code that needs to know
1388            how the mesh was generated can rely on the following checks:
1389
1390            if not self.ibz: raise("Need an IBZ sampling")
1391
1392            mpdivs, shifts = self.mpdivs_shifts
1393            if mpdivs is None: raise ValueError("Cannot handle kptrlatt with non-zero off-diagonal elements")
1394            if len(shifts) > 1: raise ValueError("Multiple shifts are not supported")
1395            # Code for mesh defined in terms of mpdivs and one shift.
1396
1397    .. inheritance-diagram:: IrredZone
1398    """
1399    #@classmethod
1400    #def from_ngkpt_or_kppa(cls, structure, ngkpt, shiftk, kptopt=1, verbose=0):
1401    #    from abipy.tools import duck
1402    #    if duck.is_listlike(ngkpt):
1403    #        return cls.from_ngkpt(structure, ngkpt, shiftk, kptopt=kptopt, verbose=verbose)
1404    #    else:
1405    #        return cls.from_kppa(structure, kppa, shiftk, kptopt=kptopt, verbose=verbose)
1406
1407    @classmethod
1408    def from_ngkpt(cls, structure, ngkpt, shiftk, kptopt=1, verbose=0):
1409        """
1410        Build an IrredZone object from (ngkpt, shift) by calling Abinit
1411        to get the list of irreducible k-points.
1412        """
1413        from abipy.abio.factories import gs_input
1414        from abipy.data.hgh_pseudos import HGH_TABLE
1415        gsinp = gs_input(structure, HGH_TABLE, spin_mode="unpolarized")
1416        ibz = gsinp.abiget_ibz(ngkpt=ngkpt, shiftk=shiftk, kptopt=kptopt, verbose=verbose)
1417        ksampling = KSamplingInfo.from_mpdivs(ngkpt, shiftk, kptopt)
1418
1419        return cls(structure.reciprocal_lattice, ibz.points, weights=ibz.weights,
1420                   names=None, ksampling=ksampling)
1421
1422    @classmethod
1423    def from_kppa(cls, structure, kppa, shiftk, kptopt=1, verbose=0):
1424        """
1425        Build an IrredZone object from (kppa, shift) by calling Abinit
1426        to get the list of irreducible k-points.
1427        """
1428        from abipy.abio.factories import gs_input
1429        from abipy.data.hgh_pseudos import HGH_TABLE
1430        gsinp = gs_input(structure, HGH_TABLE, spin_mode="unpolarized", kppa=kppa)
1431        ibz = gsinp.abiget_ibz(ngkpt=None, shiftk=shiftk, kptopt=kptopt, verbose=verbose)
1432        ksampling = KSamplingInfo.from_mpdivs(gsinp["ngkpt"], shiftk, kptopt)
1433
1434        return cls(structure.reciprocal_lattice, ibz.points, weights=ibz.weights,
1435                   names=None, ksampling=ksampling)
1436
1437    def __init__(self, reciprocal_lattice, frac_coords, weights=None, names=None, ksampling=None):
1438        """
1439        Args:
1440            reciprocal_lattice: |Lattice| object
1441            frac_coords: Array-like object with the reduced coordinates of the points.
1442            weights: Array-like with the weights of the k-points.
1443            names: List with the name of the k-points.
1444            ksampling: Info on the k-point sampling
1445        """
1446        super().__init__(reciprocal_lattice, frac_coords,
1447                         weights=weights, names=names, ksampling=ksampling)
1448
1449        # Weights must be normalized to one.
1450        wsum = self.sum_weights()
1451        if abs(wsum - 1) > 1.e-6:
1452            err_msg = ("The list of kpoints does not represent a homogeneous sampling of the BZ\n"
1453                       "Kpoint weights should sum up to one while sum_weights is %.3f\n" % wsum)
1454            print(err_msg)
1455            #raise ValueError(err_msg)
1456
1457    def __str__(self):
1458        return self.to_string()
1459
1460    def to_string(self, func=str, verbose=0, title=None):
1461        """String representation."""
1462        lines = []; app = lines.append
1463        if title is not None: app(marquee(title, mark="="))
1464
1465        if self.is_mpmesh:
1466            mpdivs, shifts = self.mpdivs_shifts
1467            d = "[%d, %d, %d]" % tuple(mpdivs)
1468            s = ", ".join("[%.1f, %.1f, %.1f]" % tuple(s) for s in shifts)
1469            app("K-mesh with divisions: %s, shifts: %s" % (d, s))
1470            app("kptopt: %s (%s)" % (self.ksampling.kptopt, kptopt2str(self.ksampling.kptopt)))
1471        else:
1472            app("nkpt: %d" % len(self))
1473            app(self.ksampling.to_string(verbose=verbose))
1474
1475        app("Number of points in the IBZ: %s" % len(self))
1476        for i, k in enumerate(self):
1477            if i > 10 and verbose == 0:
1478                app(4 * " " + "... (More than 10 k-points)")
1479                break
1480            app("%6d) [%+.3f, %+.3f, %+.3f],  weight=%.3f" % (i, k[0], k[1], k[2], k.weight))
1481
1482        return "\n".join(lines)
1483
1484    #@property
1485    #def len_bz(self):
1486    #    """Number of points in the full BZ."""
1487    #    return self.mpdivs.prod() * self.num_shifts
1488
1489    #def iter_bz_coords(self):
1490    #    """
1491    #    Generates the fractional coordinates of the points in the BZ.
1492    #    .. note:
1493    #        points are ordered in blocks, one block for each shift.
1494    #        Inside the block, points are ordered following the C convention.
1495    #    """
1496    #    for shift in self.shifts:
1497    #        for i in range(mpdivs[0]):
1498    #            x = (i + shift[0]) / mpdivs[0]
1499    #            for j in range(mpdivs[1]):
1500    #                y = (j + shift[1]) / mpdivs[1]
1501    #                for k in range(mpdivs[2]):
1502    #                    z = (k + shift[2]) / mpdivs[2]
1503    #                    yield np.array((x, y, z))
1504
1505    #def plane_cut(self, values_ibz):
1506    #    """
1507    #    Symmetrize values in the IBZ to have them on the full BZ, then
1508    #    select a slice along the specified plane E.g. plane = (1,1,0).
1509    #    """
1510    #    assert len(values_ibz) == len(self)
1511    #    #indices =
1512    #    z0 = 0
1513    #    plane = np.empty((self.nx, self.ny))
1514    #    kx, ky = range(self.nx), range(self.ny)
1515    #    for x in kx:
1516    #        for y in ky:
1517    #            ibz_idx = self.map_xyz2ibz[x, y, z0]
1518    #            plane[x, y] = values_ibz[ibz_idx]
1519    #    kx, ky = np.meshgrid(kx, ky)
1520    #    return kx, ky, plane
1521
1522
1523class KSamplingInfo(AttrDict):
1524    """
1525    Store metadata defining the k-point sampling according to the abinit conventions.
1526    One should pass through one of the class methods to create the class, avoid calling __init__ directly.
1527    """
1528
1529    KNOWN_KEYS = set([
1530        "mpdivs",          # Mesh divisions. Defined only if we have a sampling with diagonal kptrlatt else None.
1531        "kptrlatt",        # [3, 3] matrix defined only if we have a sampling else None.
1532        "kptrlatt_orig",   # Original set of shifts. Defined only if we have a sampling else None.
1533        "shifts",          # Actual shifts (Usually one). Defined only if we have a sampling else None.
1534        "shifts_orig",     # Original shifts specified by the user. Defined only if we have a sampling else None.
1535        "kptopt",          # Options for k-point generation. Negative if we have a k-path (nbounds - 1).
1536    ])
1537
1538    @classmethod
1539    def as_ksampling(cls, obj):
1540        """"
1541        Convert obj into a :class:`KSamplingInfo` instance.
1542        Accepts: :class:`KSamplingInfo` instance, None (if info are not available) or dict-like object.
1543        """
1544        if isinstance(obj, cls): return obj
1545        if obj is None:
1546            return cls(mpdivs=None,
1547                       kptrlatt=None,
1548                       kptrlatt_orig=None,
1549                       shifts=None,
1550                       shifts_orig=None,
1551                       kptopt=0,
1552            )
1553
1554        # Assume dict-like object.
1555        try:
1556            return cls(**obj)
1557        except Exception as exc:
1558            raise TypeError("Don't know how to convert `%s` into KSamplingInfo object:\n%s" % (type(obj), str(exc)))
1559
1560    @classmethod
1561    def from_mpdivs(cls, mpdivs, shifts, kptopt):
1562        """
1563        Homogeneous sampling specified in terms of ``mpdivs`` (ngkpt in abinit),
1564        the set of ``shifts`` and the value of ``kptopt``.
1565        """
1566        kptrlatt = kptrlatt_orig = np.diag(mpdivs)
1567        shifts = shifts_orig = np.reshape(np.array(shifts), (-1, 3))
1568
1569        return cls(mpdivs=mpdivs, shifts=shifts, shifts_orig=shifts_orig,
1570                   kptrlatt=kptrlatt, kptrlatt_orig=kptrlatt_orig, kptopt=kptopt)
1571
1572    @classmethod
1573    def from_kptrlatt(cls, kptrlatt, shifts, kptopt):
1574        """
1575        Homogeneous sampling specified in terms of ``kptrlatt``
1576        the set of ``shifts`` and the value of ``kptopt``.
1577        """
1578        kptrlatt = kptrlatt_orig = np.reshape(kptrlatt, (3, 3))
1579        shifts = shifts_orig = np.reshape(np.array(shifts), (-1, 3))
1580        # Test if kptrlatt is diagonal.
1581        mpdivs = None if not is_diagonal(kptrlatt) else np.diag(kptrlatt)
1582
1583        return cls(mpdivs=mpdivs, shifts=shifts, shifts_orig=shifts_orig,
1584                   kptrlatt=kptrlatt, kptrlatt_orig=kptrlatt_orig, kptopt=kptopt)
1585
1586    @classmethod
1587    def from_kbounds(cls, kbounds):
1588        """
1589        Metadata associated to a k-path specified in terms of boundaries.
1590        """
1591        mpdivs, kptrlatt, kptrlatt_orig, shifts, shifts_orig = 5 * (None,)
1592        kptopt = - (len(np.reshape(kbounds, (-1, 3))) - 1)  # Note -1
1593
1594        return cls(mpdivs=mpdivs, shifts=shifts, shifts_orig=shifts_orig,
1595                   kptrlatt=kptrlatt, kptrlatt_orig=kptrlatt_orig, kptopt=kptopt)
1596
1597    def __init__(self, *args, **kwargs):
1598        super().__init__(*args, **kwargs)
1599        for k in self:
1600            if k not in self.KNOWN_KEYS:
1601                raise ValueError("Unknow key %s" % k)
1602
1603        # FIXME: monkhorst_pack_folding is not written in e.g. DEN.nc files
1604        # so we get crazy results because of netCDF4._default_fillvals
1605        # This part set the value of mpdivs from kptrlatt.
1606        if self["mpdivs"] is not None and np.any(np.abs(self["mpdivs"]) > 1e+6):
1607            if self.kptrlatt_orig is not None:
1608                # We have a sampling
1609                if np.all(self.kptrlatt_orig == self.kptrlatt) and is_diagonal(self.kptrlatt):
1610                    self["mpdivs"] = np.diag(self.kptrlatt)
1611                else:
1612                    self["mpdivs"] = None
1613#                    import warnings
1614#                    warnings.warn("""
1615#monkhorst_pack_folding variables has not been written to netcdf file.
1616#Received {mpdivs}
1617#Setting mpdivs to None, this may create problems in post-processing tools.
1618#If needed, use python netcdf to change the value of `monkhorst_pack_folding`""".format(mpdivs=self["mpdivs"]))
1619
1620    def __str__(self):
1621        return self.to_string()
1622
1623    def to_string(self, verbose=0, title=None, **kwargs):
1624        """String representation."""
1625        lines = []; app = lines.append
1626        if title is not None: app(marquee(title, mark="="))
1627
1628        if self.is_mesh:
1629            if self.has_diagonal_kptrlatt:
1630                app("mpdivs: %s with shifts %s and kptopt: %s" % (str(self.mpdivs), str(self.shifts), self.kptopt))
1631            else:
1632                app("kptrlatt: %s" % str(self.kptrlatt))
1633                app("shifts: %s" % str(self.shifts))
1634                app("kptrlatt_orig: %s" % str(self.kptrlatt_orig))
1635                app("shifts_orig: %s" % str(self.shifts_orig))
1636                app("kptopt: %s (%s)" % (str(self.kptopt), kptopt2str(self.kptopt)))
1637
1638        elif self.is_path:
1639            app("Path with kptopt: %s" % self.kptopt)
1640        else:
1641            app("Neither mesh or path!")
1642
1643        return "\n".join(lines)
1644
1645    @property
1646    def is_mesh(self):
1647        """True if we have a mesh in the BZ."""
1648        return self.kptopt > 0 and (self.mpdivs is not None or self.kptrlatt is not None)
1649
1650    @property
1651    def is_path(self):
1652        """True if we have a path in the BZ."""
1653        return self.kptopt < 0
1654
1655    #@property
1656    #def is_homogeneous(self):
1657    #    """True if we have a homogeneous sampling of the BZ."""
1658    #    return self.kptopt > 0 and (self.mpdivs is not None or self.kptrlatt is not None)
1659
1660    @property
1661    def has_diagonal_kptrlatt(self):
1662        """True if sampling with diagonal kptrlatt."""
1663        if self.kptrlatt is None: return False
1664        return is_diagonal(self.kptrlatt)
1665
1666
1667class KpointsReaderMixin(object):
1668    """
1669    Mixin class that provides methods for reading k-point data from a netcdf
1670    file written according to the ETSF-IO specifications.
1671    """
1672    def read_kpoints(self):
1673        """
1674        Factory function: returns an instance of :class:`Kpath` or :class:`IrredZone`
1675        depending on the content of the Netcdf file. Main entry point for client code.
1676        """
1677        structure = self.read_structure()
1678        frac_coords = self.read_kfrac_coords()
1679        weights = self.read_kweights()
1680        ksampling = self.read_ksampling_info()
1681
1682        #if ksampling.kptopt < 0:
1683        if ksampling.kptopt < 0 or np.all(weights == 1):
1684            # We have a path in the BZ.
1685            kpath = Kpath(structure.reciprocal_lattice, frac_coords, ksampling=ksampling)
1686            for kpoint in kpath:
1687                kpoint.set_name(structure.findname_in_hsym_stars(kpoint))
1688            return kpath
1689
1690        # FIXME
1691        # Quick and dirty hack to allow the reading of the k-points from WFK files
1692        # where info on the sampling is missing. I will regret it but at present
1693        # is the only solution I found (changes in the ETSF-IO part of Abinit are needed)
1694        #if ksampling.is_homogeneous or abs(sum(weights) - 1.0) < 1.e-6:
1695        #if np.any(ksampling.kptrlatt_orig != 0) or abs(sum(weights) - 1.0) < 1.e-6:
1696
1697        #if np.any(ksampling.kptrlatt_orig != 0):
1698        # We have a homogeneous sampling of the BZ.
1699        return IrredZone(structure.reciprocal_lattice, frac_coords, weights=weights, ksampling=ksampling)
1700
1701    def read_ksampling_info(self):
1702        """
1703        Read information on the k-point sampling. Return :class:`KSamplingInfo` object.
1704        """
1705        # FIXME: in v8.0, the SIGRES files does not have kptopt, kptrlatt_orig and shiftk_orig
1706        kptrlatt = self.read_kptrlatt()
1707        shifts = self.read_kshifts()
1708
1709        return KSamplingInfo(
1710            mpdivs=self.read_kmpdivs(),
1711            kptrlatt=kptrlatt,
1712            kptrlatt_orig=self.read_value("kptrlatt_orig", default=kptrlatt),
1713            shifts=shifts,
1714            shifts_orig=self.read_value("shiftk_orig", default=shifts),
1715            kptopt=int(self.read_value("kptopt", default=0)),
1716        )
1717
1718    def read_kfrac_coords(self):
1719        """Fractional coordinates of the k-points"""
1720        return self.read_value("reduced_coordinates_of_kpoints")
1721
1722    def read_kweights(self):
1723        """Returns the weight of the k-points. None if not found."""
1724        return self.read_value("kpoint_weights")
1725
1726    def read_kshifts(self):
1727        """Returns the shifts of the k-mesh in reduced coordinates. None if not found."""
1728        try:
1729            return self.read_value("shiftk")
1730        except self.Error:
1731            return self.read_value("kpoint_grid_shift")
1732
1733    def read_kmpdivs(self):
1734        """Returns the Monkhorst-Pack divisions defining the mesh. None if not found."""
1735
1736        if "monkhorst_pack_folding" in self.rootgrp.variables:
1737            return self.none_if_masked_array(self.read_value("monkhorst_pack_folding"))
1738        else:
1739            kptrlatt = self.read_kptrlatt()
1740            kmpdivs = np.diag(kptrlatt)
1741            for i in range(3):
1742                for j in range(3):
1743                    if i != j and kptrlatt[i, j] != 0: kmpdivs = None
1744            return kmpdivs
1745
1746    def read_kptrlatt(self):
1747        """Returns ABINIT variable kptrlatt. None if not found."""
1748        try:
1749            return self.read_value("kptrlatt")
1750        except self.Error:
1751            return self.read_value("kpoint_grid_vectors")
1752
1753
1754class KpointsReader(ETSF_Reader, KpointsReaderMixin):
1755    """
1756    This object reads k-point data from a netcdf file.
1757
1758    .. inheritance-diagram:: KpointsReader
1759    """
1760
1761
1762class Ktables(object):
1763    """
1764    Call spglib to compute the k-points in the IBZ with the corresponding weights
1765    as well as the mapping BZ --> IBZ.
1766
1767    Args:
1768        mesh:
1769        is_shift:
1770
1771    Attributes:
1772
1773        mesh
1774        is_shift
1775        ibz:
1776        nibz
1777        weights:
1778        bz:
1779        nbz
1780        grid:
1781    """
1782    def __init__(self, structure, mesh, is_shift, has_timrev):
1783        """
1784
1785        Args:
1786            structure
1787            mesh
1788            is_shift
1789            has_timrev
1790        """
1791        import spglib as spg
1792        self.mesh = np.array(mesh)
1793        self.is_shift = is_shift
1794        self.has_timrev = has_timrev
1795        cell = (structure.lattice.matrix, structure.frac_coords, structure.atomic_numbers)
1796
1797        mapping, self.grid = spg.get_ir_reciprocal_mesh(self.mesh, cell,
1798            is_shift=self.is_shift, is_time_reversal=self.has_timrev, symprec=_SPGLIB_SYMPREC)
1799
1800        uniq, self.weights = np.unique(mapping, return_counts=True)
1801        self.weights = np.asarray(self.weights, dtype=float) / len(self.grid)
1802        self.nibz = len(uniq)
1803        self.kshift = [0., 0., 0.] if is_shift is None else 0.5 * np.asarray(is_shift)
1804        self.ibz = (self.grid[uniq] + self.kshift) / self.mesh
1805        self.bz = (self.grid + self.kshift) / self.mesh
1806        self.nbz = len(self.bz)
1807
1808        # All k-points and mapping to ir-grid points.
1809        # FIXME This is slow.
1810        self.bz2ibz = np.empty(len(self.bz), dtype=int)
1811        for ik_bz, ir_gp_id in enumerate(mapping):
1812            inds = np.where(uniq == ir_gp_id)
1813            assert len(inds) == 1
1814            self.bz2ibz[ik_bz] = inds[0]
1815
1816    def __str__(self):
1817        return self.to_string()
1818
1819    def to_string(self, verbose=0, title=None, **kwargs):
1820        """String representation"""
1821        lines = collections.deque(); app = lines.append
1822        if title is not None: app(marquee(title, mark="="))
1823
1824        app("mesh %s, shift %s, time-reversal: %s, Irred points: %d" % (
1825            self.mesh, self.kshift, self.has_timrev, self.nibz))
1826
1827        app("Irreducible k-points with number of points in star:\n")
1828        for ik, kpt in enumerate(self.ibz):
1829            app("%s: [%9.6f, %9.6f, %9.6f], nstar: %d" % (ik, kpt[0], kpt[1], kpt[2], self.weights[ik] * self.nbz))
1830
1831        return "\n".join(lines)
1832
1833    def print_bz2ibz(self, file=sys.stdout):
1834        """Print BZ --> IBZ mapping."""
1835        print("BZ points --> IBZ points mapping", file=file)
1836        for ik_bz, ik_ibz in enumerate(self.bz2ibz):
1837            print("%6d) [%9.6f, %9.6f, %9.6f], ===> %6d) [%9.6f, %9.6f, %9.6f]," %
1838                (ik_bz, self.bz[ik_ibz][0], self.bz[ik_ibz][1], self.bz[ik_ibz][2],
1839                ik_ibz, self.ibz[ik_ibz][0], self.ibz[ik_ibz][1], self.ibz[ik_ibz][2]), file=file)
1840
1841
1842def dist_point_from_line(x0, x1, x2):
1843    """
1844    Return distance from point x0 to line x1 - x2. Cartesian coordinates are used.
1845    See <http://mathworld.wolfram.com/Point-LineDistance3-Dimensional.html>
1846    """
1847    denom = x2 - x1
1848    denomabs = np.sqrt(np.dot(denom, denom))
1849    numer = np.cross(x0 - x1, x0 - x2)
1850    numerabs = np.sqrt(np.dot(numer, numer))
1851    return numerabs / denomabs
1852
1853
1854def find_points_along_path(cart_bounds, cart_coords, dist_tol):
1855    """
1856    Find points in ``cart_coords`` lying on the path defined by ``cart_bounds``.
1857    Result are ordered according to distance along the path.
1858
1859    Args:
1860        cart_bounds: [N, 3] array with the boundaries of the path in Cartesian coordinates.
1861        cart_coords: [M, 3] array with the points in Cartesian coordinate
1862        dist_tol: A point is considered to be on the path if its distance from the line
1863            is less than dist_tol.
1864
1865    Return: namedtuple with the following attributes:
1866
1867        (ikfound, dist_list, path_ticks)
1868
1869        ikfound is a numpy array with the indices of the points lying on the path. Empty if no point is found.
1870        dist_list: numpy array with the distance of the points along the line.
1871        path_ticks: numpy array with the ticks associated to the input k-path.
1872    """
1873    ikfound, dist_list, path_ticks = [], [], [0]
1874
1875    dl = 0  # cumulative length of the path
1876    for ibound, x0 in enumerate(cart_bounds[:-1]):
1877        x1 = cart_bounds[ibound + 1]
1878        B = x0 - x1
1879        #B = x1 - x0
1880        dk = np.sqrt(np.dot(B,B))
1881        #print("x0", x0, "x1", x1)
1882        path_ticks.append(path_ticks[ibound] + dk)
1883        for ik, k in enumerate(cart_coords):
1884            dist = dist_point_from_line(k, x0, x1)
1885            #print(frac_coords[ik], dist)
1886            if dist > dist_tol: continue
1887            # k-point is on the cart_bounds
1888            A = x0 - k
1889            #A = k - x0
1890            x = np.dot(A, B)/dk
1891            #print("k-x0", A, "B", B)
1892            #print(frac_coords[ik], x, x > 0 and x < dist_tol + dk)
1893            if dist_tol + dk >= x >= 0:
1894                # k-point is within the cart_bounds range
1895                # append k-point coordinate along the cart_bounds while avoing duplicate entries.
1896                if ikfound and ik == ikfound[-1]: continue
1897                ikfound.append(ik)
1898                dist_list.append(x + dl)
1899
1900        dl = dl + dk
1901
1902    # Sort dist_list and ikfound by cumulative length while removing possible duplicated entries.
1903    dist_list, isort = np.unique(np.array(dist_list).round(decimals=5), return_index=True)
1904
1905    return dict2namedtuple(ikfound=np.array(ikfound)[isort],
1906                           dist_list=dist_list,
1907                           path_ticks=np.array(path_ticks))
1908
1909
1910def build_segments(k0_list, npts, step, red_dirs, reciprocal_lattice):
1911    """
1912    For each point in k0_list, build a line passing through the point for each
1913    reduced direction in red_dir. Each line consists of `npts` points with step `step` in Ang-1
1914    and is centered on the k-point. Return: (nk0_list, len(red_dirs) * npts, 3) array with fractional coordinates.
1915
1916    Args:
1917        k0_list: List of k-points in reduced coordinates.
1918        npts: Number of points in each segment.
1919        step: Step in Ang-1
1920        red_dirs: List of reduced directions
1921        reciprocal_lattice: Reciprocal lattice (from structure.reciprocal_lattice)
1922    """
1923    k0_list = np.reshape(k0_list, (-1, 3))
1924    red_dirs = np.reshape(red_dirs, (-1, 3))
1925    kpts = []
1926    for kpoint in k0_list:
1927        kpoint = Kpoint.as_kpoint(kpoint, reciprocal_lattice)
1928        # Build segments passing through this kpoint (work in Cartesian coords)
1929        for rdir in red_dirs:
1930            bvers = reciprocal_lattice.matrix.T @ rdir
1931            #bvers = reciprocal_lattice.get_cartesian_coords(rdir)
1932            bvers /= np.sqrt(np.dot(bvers, bvers))
1933            kstart = kpoint.cart_coords - bvers * (npts // 2) * step
1934            for ii in range(npts):
1935                kpts.append(kstart + ii * step * bvers)
1936
1937    # Cart --> Frac
1938    out = reciprocal_lattice.get_fractional_coords(kpts)
1939    return np.reshape(out, (len(k0_list), len(red_dirs) * npts, 3))
1940