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