Coverage for ase / dft / kpoints.py: 86.80%
341 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-04 10:20 +0000
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-04 10:20 +0000
1# fmt: off
2from __future__ import annotations
4import re
5import warnings
6from typing import TYPE_CHECKING
8import numpy as np
10from ase import Atoms
11from ase.cell import Cell
12from ase.utils import jsonable
14if TYPE_CHECKING:
15 from ase.spectrum.band_structure import BandStructure
18def monkhorst_pack(size):
19 """Construct a uniform sampling of k-space of given size."""
20 if np.less_equal(size, 0).any():
21 raise ValueError(f'Illegal size: {list(size)}')
22 kpts = np.indices(size).transpose((1, 2, 3, 0)).reshape((-1, 3))
23 return (kpts + 0.5) / size - 0.5
26def get_monkhorst_pack_size_and_offset(kpts):
27 """Find Monkhorst-Pack size and offset.
29 Returns (size, offset), where::
31 kpts = monkhorst_pack(size) + offset.
33 The set of k-points must not have been symmetry reduced."""
35 if len(kpts) == 1:
36 return np.ones(3, int), np.array(kpts[0], dtype=float)
38 size = np.zeros(3, int)
39 for c in range(3):
40 # Determine increment between k-points along current axis
41 delta = max(np.diff(np.sort(kpts[:, c])))
43 # Determine number of k-points as inverse of distance between kpoints
44 if delta > 1e-8:
45 size[c] = int(round(1.0 / delta))
46 else:
47 size[c] = 1
49 if size.prod() == len(kpts):
50 kpts0 = monkhorst_pack(size)
51 offsets = kpts - kpts0
53 # All offsets must be identical:
54 if (np.ptp(offsets, axis=0) < 1e-9).all():
55 return size, offsets[0].copy()
57 raise ValueError('Not an ASE-style Monkhorst-Pack grid!')
60def mindistance2monkhorstpack(atoms, *,
61 min_distance,
62 maxperdim=16,
63 even=True):
64 """Find a Monkhorst-Pack grid (nx, ny, nz) with lowest number of
65 k-points in the *reducible* Brillouin zone, which still satisfying
66 a given minimum distance (`min_distance`) condition in real space
67 (nx, ny, nz)-supercell.
69 Compared to ase.calculators.calculator kptdensity2monkhorstpack
70 routine, this metric is based on a physical quantity (real space
71 distance), and it doesn't depend on non-physical quantities, such as
72 the cell vectors, since basis vectors can be always transformed
73 with integer determinant one matrices. In other words, it is
74 invariant to particular choice of cell representations.
76 On orthogonal cells, min_distance = 2 * np.pi * kptdensity.
77 """
78 return _mindistance2monkhorstpack(atoms.cell, atoms.pbc,
79 min_distance, maxperdim, even)
82def _mindistance2monkhorstpack(cell, pbc_c, min_distance, maxperdim, even):
83 from ase.neighborlist import NeighborList
85 step = 2 if even else 1
86 nl = NeighborList([min_distance / 2], skin=0.0,
87 self_interaction=False, bothways=False)
89 def check(nkpts_c):
90 nl.update(Atoms('H', cell=cell @ np.diag(nkpts_c), pbc=pbc_c))
91 return len(nl.get_neighbors(0)[1]) == 0
93 def generate_mpgrids():
94 ranges = [range(step, maxperdim + 1, step)
95 if pbc else range(1, 2) for pbc in pbc_c]
96 nkpts_nc = np.column_stack([*map(np.ravel, np.meshgrid(*ranges))])
97 yield from sorted(nkpts_nc, key=np.prod)
99 try:
100 return next(filter(check, generate_mpgrids()))
101 except StopIteration:
102 raise ValueError('Could not find a proper k-point grid for the system.'
103 ' Try running with a larger maxperdim.')
106def get_monkhorst_shape(kpts):
107 warnings.warn('Use get_monkhorst_pack_size_and_offset()[0] instead.')
108 return get_monkhorst_pack_size_and_offset(kpts)[0]
111def kpoint_convert(cell_cv, skpts_kc=None, ckpts_kv=None):
112 """Convert k-points between scaled and cartesian coordinates.
114 Given the atomic unit cell, and either the scaled or cartesian k-point
115 coordinates, the other is determined.
117 The k-point arrays can be either a single point, or a list of points,
118 i.e. the dimension k can be empty or multidimensional.
119 """
120 if ckpts_kv is None:
121 icell_cv = 2 * np.pi * np.linalg.pinv(cell_cv).T
122 return np.dot(skpts_kc, icell_cv)
123 elif skpts_kc is None:
124 return np.dot(ckpts_kv, cell_cv.T) / (2 * np.pi)
125 else:
126 raise KeyError('Either scaled or cartesian coordinates must be given.')
129def parse_path_string(s):
130 """Parse compact string representation of BZ path.
132 A path string can have several non-connected sections separated by
133 commas. The return value is a list of sections where each section is a
134 list of labels.
136 Examples:
138 >>> parse_path_string('GX')
139 [['G', 'X']]
140 >>> parse_path_string('GX,M1A')
141 [['G', 'X'], ['M1', 'A']]
142 """
143 paths = []
144 for path in s.split(','):
145 names = [name
146 for name in re.split(r'([A-Z][a-z0-9]*)', path)
147 if name]
148 paths.append(names)
149 return paths
152def resolve_kpt_path_string(path, special_points):
153 paths = parse_path_string(path)
154 coords = [np.array([special_points[sym] for sym in subpath]).reshape(-1, 3)
155 for subpath in paths]
156 return paths, coords
159def resolve_custom_points(pathspec, special_points, eps):
160 """Resolve a path specification into a string.
162 The path specification is a list path segments, each segment being a kpoint
163 label or kpoint coordinate, or a single such segment.
165 Return a string representing the same path. Generic kpoint labels
166 are generated dynamically as necessary, updating the special_point
167 dictionary if necessary. The tolerance eps is used to see whether
168 coordinates are close enough to a special point to deserve being
169 labelled as such."""
170 # This should really run on Cartesian coordinates but we'll probably
171 # be lazy and call it on scaled ones.
173 # We may add new points below so take a copy of the input:
174 special_points = dict(special_points)
176 if len(pathspec) == 0:
177 return '', special_points
179 if isinstance(pathspec, str):
180 pathspec = parse_path_string(pathspec)
182 def looks_like_single_kpoint(obj):
183 if isinstance(obj, str):
184 return True
185 try:
186 arr = np.asarray(obj, float)
187 except ValueError:
188 return False
189 else:
190 return arr.shape == (3,)
192 # We accept inputs that are either
193 # 1) string notation
194 # 2) list of kpoints (each either a string or three floats)
195 # 3) list of list of kpoints; each toplevel list is a contiguous subpath
196 # Here we detect form 2 and normalize to form 3:
197 for thing in pathspec:
198 if looks_like_single_kpoint(thing):
199 pathspec = [pathspec]
200 break
202 def name_generator():
203 counter = 0
204 while True:
205 name = f'Kpt{counter}'
206 yield name
207 counter += 1
209 custom_names = name_generator()
211 labelseq = []
212 for subpath in pathspec:
213 for kpt in subpath:
214 if isinstance(kpt, str):
215 if kpt not in special_points:
216 raise KeyError('No kpoint "{}" among "{}"'
217 .format(kpt,
218 ''.join(special_points)))
219 labelseq.append(kpt)
220 continue
222 kpt = np.asarray(kpt, float)
223 if kpt.shape != (3,):
224 raise ValueError(f'Not a valid kpoint: {kpt}')
226 for key, val in special_points.items():
227 if np.abs(kpt - val).max() < eps:
228 labelseq.append(key)
229 break # Already present
230 else:
231 # New special point - search for name we haven't used yet:
232 name = next(custom_names)
233 while name in special_points:
234 name = next(custom_names)
235 special_points[name] = kpt
236 labelseq.append(name)
237 labelseq.append(',')
239 last = labelseq.pop()
240 assert last == ','
241 return ''.join(labelseq), special_points
244def normalize_special_points(special_points):
245 dct = {}
246 for name, value in special_points.items():
247 if not isinstance(name, str):
248 raise TypeError('Expected name to be a string')
249 if np.shape(value) != (3,):
250 raise ValueError('Expected 3 kpoint coordinates')
251 dct[name] = np.asarray(value, float)
252 return dct
255@jsonable('bandpath')
256class BandPath:
257 """Represents a Brillouin zone path or bandpath.
259 A band path has a unit cell, a path specification, special points,
260 and interpolated k-points. Band paths are typically created
261 indirectly using the :class:`~ase.geometry.Cell` or
262 :class:`~ase.lattice.BravaisLattice` classes:
264 >>> from ase.lattice import CUB
265 >>> path = CUB(3).bandpath()
266 >>> path
267 BandPath(path='GXMGRX,MR', cell=[3x3], special_points={GMRX}, kpts=[40x3])
269 Band paths support JSON I/O:
271 >>> from ase.io.jsonio import read_json
272 >>> path.write('mybandpath.json')
273 >>> read_json('mybandpath.json')
274 BandPath(path='GXMGRX,MR', cell=[3x3], special_points={GMRX}, kpts=[40x3])
276 """
278 def __init__(self, cell, kpts=None,
279 special_points=None, path=None):
280 if kpts is None:
281 kpts = np.empty((0, 3))
283 if special_points is None:
284 special_points = {}
285 else:
286 special_points = normalize_special_points(special_points)
288 if path is None:
289 path = ''
291 cell = Cell(cell)
292 self._cell = cell
293 kpts = np.asarray(kpts)
294 assert kpts.ndim == 2 and kpts.shape[1] == 3 and kpts.dtype == float
295 self._icell = self.cell.reciprocal()
296 self._kpts = kpts
297 self._special_points = special_points
298 if not isinstance(path, str):
299 raise TypeError(f'path must be a string; was {path!r}')
300 self._path = path
302 @property
303 def cell(self) -> Cell:
304 """The :class:`~ase.cell.Cell` of this BandPath."""
305 return self._cell
307 @property
308 def icell(self) -> Cell:
309 """Reciprocal cell of this BandPath as a :class:`~ase.cell.Cell`."""
310 return self._icell
312 @property
313 def kpts(self) -> np.ndarray:
314 """The kpoints of this BandPath as an array of shape (nkpts, 3).
316 The kpoints are given in units of the reciprocal cell."""
317 return self._kpts
319 @property
320 def special_points(self) -> dict[str, np.ndarray]:
321 """Special points of this BandPath as a dictionary.
323 The dictionary maps names (such as `'G'`) to kpoint coordinates
324 in units of the reciprocal cell as a 3-element numpy array.
326 It's unwise to edit this dictionary directly. If you need that,
327 consider deepcopying it."""
328 return self._special_points
330 @property
331 def path(self) -> str:
332 """The string specification of this band path.
334 This is a specification of the form `'GXWKGLUWLK,UX'`.
336 Comma marks a discontinuous jump: K is not connected to U."""
337 return self._path
339 def transform(self, op: np.ndarray) -> BandPath:
340 """Apply 3x3 matrix to this BandPath and return new BandPath.
342 This is useful for converting the band path to another cell.
343 The operation will typically be a permutation/flipping
344 established by a function such as Niggli reduction."""
345 # XXX acceptable operations are probably only those
346 # who come from Niggli reductions (permutations etc.)
347 #
348 # We should insert a check.
349 # I wonder which operations are valid? They won't be valid
350 # if they change lengths, volume etc.
351 special_points = {
352 name: value @ op for name, value in self.special_points.items()
353 }
354 return BandPath(op.T @ self.cell, kpts=self.kpts @ op,
355 special_points=special_points,
356 path=self.path)
358 def todict(self):
359 return {'kpts': self.kpts,
360 'special_points': self.special_points,
361 'labelseq': self.path,
362 'cell': self.cell}
364 def interpolate(
365 self,
366 path: str = None,
367 npoints: int = None,
368 special_points: dict[str, np.ndarray] = None,
369 density: float = None,
370 ) -> BandPath:
371 """Create new bandpath, (re-)interpolating kpoints from this one."""
372 if path is None:
373 path = self.path
375 if special_points is None:
376 special_points = self.special_points
378 _pathnames, pathcoords = resolve_kpt_path_string(path, special_points)
379 kpts, _x, _X = paths2kpts(pathcoords, self.cell, npoints, density)
380 return BandPath(self.cell, kpts, path=path,
381 special_points=special_points)
383 def _scale(self, coords):
384 return np.dot(coords, self.icell)
386 def __repr__(self):
387 return ('{}(path={}, cell=[3x3], special_points={{{}}}, kpts=[{}x3])'
388 .format(self.__class__.__name__,
389 repr(self.path),
390 ''.join(sorted(self.special_points)),
391 len(self.kpts)))
393 def cartesian_kpts(self) -> np.ndarray:
394 """Get Cartesian kpoints from this bandpath."""
395 return self._scale(self.kpts)
397 def __iter__(self):
398 """XXX Compatibility hack for bandpath() function.
400 bandpath() now returns a BandPath object, which is a Good
401 Thing. However it used to return a tuple of (kpts, x_axis,
402 special_x_coords), and people would use tuple unpacking for
403 those.
405 This function makes tuple unpacking work in the same way.
406 It will be removed in the future.
408 """
409 warnings.warn('Please do not use (kpts, x, X) = bandpath(...). '
410 'Use path = bandpath(...) and then kpts = path.kpts and '
411 '(x, X, labels) = path.get_linear_kpoint_axis().')
412 yield self.kpts
414 x, xspecial, _ = self.get_linear_kpoint_axis()
415 yield x
416 yield xspecial
418 def __getitem__(self, index):
419 # Temp compatibility stuff, see __iter__
420 return tuple(self)[index]
422 def get_linear_kpoint_axis(self, eps=1e-5):
423 """Define x axis suitable for plotting a band structure.
425 See :func:`ase.dft.kpoints.labels_from_kpts`."""
427 index2name = self._find_special_point_indices(eps)
428 indices = sorted(index2name)
429 labels = [index2name[index] for index in indices]
430 xcoords, special_xcoords = indices_to_axis_coords(
431 indices, self.kpts, self.cell)
432 return xcoords, special_xcoords, labels
434 def _find_special_point_indices(self, eps):
435 """Find indices of kpoints which are close to special points.
437 The result is returned as a dictionary mapping indices to labels."""
438 # XXX must work in Cartesian coordinates for comparison to eps
439 # to fully make sense!
440 index2name = {}
441 nkpts = len(self.kpts)
443 for name, kpt in self.special_points.items():
444 displacements = self.kpts - kpt[np.newaxis, :]
445 distances = np.linalg.norm(displacements, axis=1)
446 args = np.argwhere(distances < eps)
447 for arg in args.flat:
448 dist = distances[arg]
449 # Check if an adjacent point exists and is even closer:
450 neighbours = distances[max(arg - 1, 0):min(arg + 1, nkpts - 1)]
451 if not any(neighbours < dist):
452 index2name[arg] = name
454 return index2name
456 def plot(self, **plotkwargs):
457 """Visualize this bandpath.
459 Plots the irreducible Brillouin zone and this bandpath."""
460 import ase.dft.bz as bz
462 # We previously had a "dimension=3" argument which is now unused.
463 plotkwargs.pop('dimension', None)
465 special_points = self.special_points
466 labelseq, coords = resolve_kpt_path_string(self.path,
467 special_points)
469 paths = []
470 points_already_plotted = set()
471 for subpath_labels, subpath_coords in zip(labelseq, coords):
472 subpath_coords = np.array(subpath_coords)
473 points_already_plotted.update(subpath_labels)
474 paths.append((subpath_labels, self._scale(subpath_coords)))
476 # Add each special point as a single-point subpath if they were
477 # not plotted already:
478 for label, point in special_points.items():
479 if label not in points_already_plotted:
480 paths.append(([label], [self._scale(point)]))
482 kw = {'vectors': True,
483 'pointstyle': {'marker': '.'}}
485 kw.update(plotkwargs)
486 return bz.bz_plot(self.cell, paths=paths,
487 points=self.cartesian_kpts(),
488 **kw)
490 def free_electron_band_structure(self, **kwargs) -> BandStructure:
491 """Return band structure of free electrons for this bandpath.
493 Keyword arguments are passed to
494 :class:`~ase.calculators.test.FreeElectrons`.
496 This is for mostly testing and visualization."""
497 from ase.calculators.test import FreeElectrons
498 from ase.spectrum.band_structure import calculate_band_structure
499 atoms = Atoms(cell=self.cell, pbc=True)
500 atoms.calc = FreeElectrons(**kwargs)
501 bs = calculate_band_structure(atoms, path=self)
502 return bs
505def bandpath(path, cell, npoints=None, density=None, special_points=None,
506 eps=2e-4):
507 """Make a list of kpoints defining the path between the given points.
509 path: list or str
510 Can be:
512 * a string that parse_path_string() understands: 'GXL'
513 * a list of BZ points: [(0, 0, 0), (0.5, 0, 0)]
514 * or several lists of BZ points if the the path is not continuous.
515 cell: 3x3
516 Unit cell of the atoms.
517 npoints: int
518 Length of the output kpts list. If too small, at least the beginning
519 and ending point of each path segment will be used. If None (default),
520 it will be calculated using the supplied density or a default one.
521 density: float
522 k-points per 1/A on the output kpts list. If npoints is None,
523 the number of k-points in the output list will be:
524 npoints = density * path total length (in Angstroms).
525 If density is None (default), use 5 k-points per A⁻¹.
526 If the calculated npoints value is less than 50, a minimum value of 50
527 will be used.
528 special_points: dict or None
529 Dictionary mapping names to special points. If None, the special
530 points will be derived from the cell.
531 eps: float
532 Precision used to identify Bravais lattice, deducing special points.
534 You may define npoints or density but not both.
536 Return a :class:`~ase.dft.kpoints.BandPath` object."""
538 cell = Cell.ascell(cell)
539 return cell.bandpath(path, npoints=npoints, density=density,
540 special_points=special_points, eps=eps)
543DEFAULT_KPTS_DENSITY = 5 # points per 1/Angstrom
546def paths2kpts(paths, cell, npoints=None, density=None):
547 if npoints is not None and density is not None:
548 raise ValueError('You may define npoints or density, but not both.')
549 points = np.concatenate(paths)
550 dists = points[1:] - points[:-1]
551 lengths = [np.linalg.norm(d) for d in kpoint_convert(cell, skpts_kc=dists)]
553 i = 0
554 for path in paths[:-1]:
555 i += len(path)
556 lengths[i - 1] = 0
558 length = sum(lengths)
560 if npoints is None:
561 if density is None:
562 density = DEFAULT_KPTS_DENSITY
563 # Set npoints using the length of the path
564 npoints = int(round(length * density))
566 kpts = []
567 x0 = 0
568 x = []
569 X = [0]
570 for P, d, L in zip(points[:-1], dists, lengths):
571 diff = length - x0
572 if abs(diff) < 1e-6:
573 n = 0
574 else:
575 n = max(2, int(round(L * (npoints - len(x)) / diff)))
577 for t in np.linspace(0, 1, n)[:-1]:
578 kpts.append(P + t * d)
579 x.append(x0 + t * L)
580 x0 += L
581 X.append(x0)
582 if len(points):
583 kpts.append(points[-1])
584 x.append(x0)
586 if len(kpts) == 0:
587 kpts = np.empty((0, 3))
589 return np.array(kpts), np.array(x), np.array(X)
592get_bandpath = bandpath # old name
595def find_bandpath_kinks(cell, kpts, eps=1e-5):
596 """Find indices of those kpoints that are not interior to a line segment."""
597 # XXX Should use the Cartesian kpoints.
598 # Else comparison to eps will be anisotropic and hence arbitrary.
599 diffs = kpts[1:] - kpts[:-1]
600 kinks = abs(diffs[1:] - diffs[:-1]).sum(1) > eps
601 N = len(kpts)
602 indices = []
603 if N > 0:
604 indices.append(0)
605 indices.extend(np.arange(1, N - 1)[kinks])
606 indices.append(N - 1)
607 return indices
610def labels_from_kpts(kpts, cell, eps=1e-5, special_points=None):
611 """Get an x-axis to be used when plotting a band structure.
613 The first of the returned lists can be used as a x-axis when plotting
614 the band structure. The second list can be used as xticks, and the third
615 as xticklabels.
617 Parameters:
619 kpts: list
620 List of scaled k-points.
622 cell: list
623 Unit cell of the atomic structure.
625 Returns:
627 Three arrays; the first is a list of cumulative distances between k-points,
628 the second is x coordinates of the special points,
629 the third is the special points as strings.
630 """
632 if special_points is None:
633 special_points = get_special_points(cell)
634 points = np.asarray(kpts)
635 # XXX Due to this mechanism, we are blind to special points
636 # that lie on straight segments such as [K, G, -K].
637 indices = find_bandpath_kinks(cell, kpts, eps=1e-5)
639 labels = []
640 for kpt in points[indices]:
641 for label, k in special_points.items():
642 if abs(kpt - k).sum() < eps:
643 break
644 else:
645 # No exact match. Try modulus 1:
646 for label, k in special_points.items():
647 if abs((kpt - k) % 1).sum() < eps:
648 break
649 else:
650 label = '?'
651 labels.append(label)
653 xcoords, ixcoords = indices_to_axis_coords(indices, points, cell)
654 return xcoords, ixcoords, labels
657def indices_to_axis_coords(indices, points, cell):
658 jump = False # marks a discontinuity in the path
659 xcoords = [0]
660 for i1, i2 in zip(indices[:-1], indices[1:]):
661 if not jump and i1 + 1 == i2:
662 length = 0
663 jump = True # we don't want two jumps in a row
664 else:
665 diff = points[i2] - points[i1]
666 length = np.linalg.norm(kpoint_convert(cell, skpts_kc=diff))
667 jump = False
668 xcoords.extend(np.linspace(0, length, i2 - i1 + 1)[1:] + xcoords[-1])
670 xcoords = np.array(xcoords)
671 return xcoords, xcoords[indices]
674special_paths = {
675 'cubic': 'GXMGRX,MR',
676 'fcc': 'GXWKGLUWLK,UX',
677 'bcc': 'GHNGPH,PN',
678 'tetragonal': 'GXMGZRAZXR,MA',
679 'orthorhombic': 'GXSYGZURTZ,YT,UX,SR',
680 'hexagonal': 'GMKGALHA,LM,KH',
681 'monoclinic': 'GYHCEM1AXH1,MDZ,YD',
682 'rhombohedral type 1': 'GLB1,BZGX,QFP1Z,LP',
683 'rhombohedral type 2': 'GPZQGFP1Q1LZ'}
686def get_special_points(cell, lattice=None, eps=2e-4):
687 """Return dict of special points.
689 The definitions are from a paper by Wahyu Setyawana and Stefano
690 Curtarolo::
692 https://doi.org/10.1016/j.commatsci.2010.05.010
694 cell: 3x3 ndarray
695 Unit cell.
696 lattice: str
697 Optionally check that the cell is one of the following: cubic, fcc,
698 bcc, orthorhombic, tetragonal, hexagonal or monoclinic.
699 eps: float
700 Tolerance for cell-check.
701 """
703 if isinstance(cell, str):
704 warnings.warn('Please call this function with cell as the first '
705 'argument')
706 lattice, cell = cell, lattice
708 cell = Cell.ascell(cell)
709 # We create the bandpath because we want to transform the kpoints too,
710 # from the canonical cell to the given one.
711 #
712 # Note that this function is missing a tolerance, epsilon.
713 path = cell.bandpath(npoints=0)
714 return path.special_points
717def monkhorst_pack_interpolate(path, values, icell, bz2ibz,
718 size, offset=(0, 0, 0), pad_width=2):
719 """Interpolate values from Monkhorst-Pack sampling.
721 `monkhorst_pack_interpolate` takes a set of `values`, for example
722 eigenvalues, that are resolved on a Monkhorst Pack k-point grid given by
723 `size` and `offset` and interpolates the values onto the k-points
724 in `path`.
726 Note
727 ----
728 For the interpolation to work, path has to lie inside the domain
729 that is spanned by the MP kpoint grid given by size and offset.
731 To try to ensure this we expand the domain slightly by adding additional
732 entries along the edges and sides of the domain with values determined by
733 wrapping the values to the opposite side of the domain. In this way we
734 assume that the function to be interpolated is a periodic function in
735 k-space. The padding width is determined by the `pad_width` parameter.
737 Parameters
738 ----------
739 path: (nk, 3) array-like
740 Desired path in units of reciprocal lattice vectors.
741 values: (nibz, ...) array-like
742 Values on Monkhorst-Pack grid.
743 icell: (3, 3) array-like
744 Reciprocal lattice vectors.
745 bz2ibz: (nbz,) array-like of int
746 Map from nbz points in BZ to nibz reduced points in IBZ.
747 size: (3,) array-like of int
748 Size of Monkhorst-Pack grid.
749 offset: (3,) array-like
750 Offset of Monkhorst-Pack grid.
751 pad_width: int
752 Padding width to aid interpolation
754 Returns
755 -------
756 (nbz,) array-like
757 *values* interpolated to *path*.
758 """
759 from scipy.interpolate import LinearNDInterpolator
761 path = (np.asarray(path) + 0.5) % 1 - 0.5
762 path = np.dot(path, icell)
764 # Fold out values from IBZ to BZ:
765 v = np.asarray(values)[bz2ibz]
766 v = v.reshape(tuple(size) + v.shape[1:])
768 # Create padded Monkhorst-Pack grid:
769 size = np.asarray(size)
770 i = (np.indices(size + 2 * pad_width)
771 .transpose((1, 2, 3, 0)).reshape((-1, 3)))
772 k = (i - pad_width + 0.5) / size - 0.5 + offset
773 k = np.dot(k, icell)
775 # Fill in boundary values:
776 V = np.pad(v, [(pad_width, pad_width)] * 3 +
777 [(0, 0)] * (v.ndim - 3), mode="wrap")
779 interpolate = LinearNDInterpolator(k, V.reshape((-1,) + V.shape[3:]))
780 interpolated_points = interpolate(path)
782 # NaN values indicate points outside interpolation domain, if fail
783 # try increasing padding
784 assert not np.isnan(interpolated_points).any(), \
785 "Points outside interpolation domain. Try increasing pad_width."
787 return interpolated_points
790# ChadiCohen k point grids. The k point grids are given in units of the
791# reciprocal unit cell. The variables are named after the following
792# convention: cc+'<Nkpoints>'+_+'shape'. For example an 18 k point
793# sq(3)xsq(3) is named 'cc18_sq3xsq3'.
795cc6_1x1 = np.array([
796 1, 1, 0, 1, 0, 0, 0, -1, 0, -1, -1, 0, -1, 0, 0,
797 0, 1, 0]).reshape((6, 3)) / 3.0
799cc12_2x3 = np.array([
800 3, 4, 0, 3, 10, 0, 6, 8, 0, 3, -2, 0, 6, -4, 0,
801 6, 2, 0, -3, 8, 0, -3, 2, 0, -3, -4, 0, -6, 4, 0, -6, -2, 0, -6,
802 -8, 0]).reshape((12, 3)) / 18.0
804cc18_sq3xsq3 = np.array([
805 2, 2, 0, 4, 4, 0, 8, 2, 0, 4, -2, 0, 8, -4,
806 0, 10, -2, 0, 10, -8, 0, 8, -10, 0, 2, -10, 0, 4, -8, 0, -2, -8,
807 0, 2, -4, 0, -4, -4, 0, -2, -2, 0, -4, 2, 0, -2, 4, 0, -8, 4, 0,
808 -4, 8, 0]).reshape((18, 3)) / 18.0
810cc18_1x1 = np.array([
811 2, 4, 0, 2, 10, 0, 4, 8, 0, 8, 4, 0, 8, 10, 0,
812 10, 8, 0, 2, -2, 0, 4, -4, 0, 4, 2, 0, -2, 8, 0, -2, 2, 0, -2, -4,
813 0, -4, 4, 0, -4, -2, 0, -4, -8, 0, -8, 2, 0, -8, -4, 0, -10, -2,
814 0]).reshape((18, 3)) / 18.0
816cc54_sq3xsq3 = np.array([
817 4, -10, 0, 6, -10, 0, 0, -8, 0, 2, -8, 0, 6,
818 -8, 0, 8, -8, 0, -4, -6, 0, -2, -6, 0, 2, -6, 0, 4, -6, 0, 8, -6,
819 0, 10, -6, 0, -6, -4, 0, -2, -4, 0, 0, -4, 0, 4, -4, 0, 6, -4, 0,
820 10, -4, 0, -6, -2, 0, -4, -2, 0, 0, -2, 0, 2, -2, 0, 6, -2, 0, 8,
821 -2, 0, -8, 0, 0, -4, 0, 0, -2, 0, 0, 2, 0, 0, 4, 0, 0, 8, 0, 0,
822 -8, 2, 0, -6, 2, 0, -2, 2, 0, 0, 2, 0, 4, 2, 0, 6, 2, 0, -10, 4,
823 0, -6, 4, 0, -4, 4, 0, 0, 4, 0, 2, 4, 0, 6, 4, 0, -10, 6, 0, -8,
824 6, 0, -4, 6, 0, -2, 6, 0, 2, 6, 0, 4, 6, 0, -8, 8, 0, -6, 8, 0,
825 -2, 8, 0, 0, 8, 0, -6, 10, 0, -4, 10, 0]).reshape((54, 3)) / 18.0
827cc54_1x1 = np.array([
828 2, 2, 0, 4, 4, 0, 8, 8, 0, 6, 8, 0, 4, 6, 0, 6,
829 10, 0, 4, 10, 0, 2, 6, 0, 2, 8, 0, 0, 2, 0, 0, 4, 0, 0, 8, 0, -2,
830 6, 0, -2, 4, 0, -4, 6, 0, -6, 4, 0, -4, 2, 0, -6, 2, 0, -2, 0, 0,
831 -4, 0, 0, -8, 0, 0, -8, -2, 0, -6, -2, 0, -10, -4, 0, -10, -6, 0,
832 -6, -4, 0, -8, -6, 0, -2, -2, 0, -4, -4, 0, -8, -8, 0, 4, -2, 0,
833 6, -2, 0, 6, -4, 0, 2, 0, 0, 4, 0, 0, 6, 2, 0, 6, 4, 0, 8, 6, 0,
834 8, 0, 0, 8, 2, 0, 10, 4, 0, 10, 6, 0, 2, -4, 0, 2, -6, 0, 4, -6,
835 0, 0, -2, 0, 0, -4, 0, -2, -6, 0, -4, -6, 0, -6, -8, 0, 0, -8, 0,
836 -2, -8, 0, -4, -10, 0, -6, -10, 0]).reshape((54, 3)) / 18.0
838cc162_sq3xsq3 = np.array([
839 -8, 16, 0, -10, 14, 0, -7, 14, 0, -4, 14,
840 0, -11, 13, 0, -8, 13, 0, -5, 13, 0, -2, 13, 0, -13, 11, 0, -10,
841 11, 0, -7, 11, 0, -4, 11, 0, -1, 11, 0, 2, 11, 0, -14, 10, 0, -11,
842 10, 0, -8, 10, 0, -5, 10, 0, -2, 10, 0, 1, 10, 0, 4, 10, 0, -16,
843 8, 0, -13, 8, 0, -10, 8, 0, -7, 8, 0, -4, 8, 0, -1, 8, 0, 2, 8, 0,
844 5, 8, 0, 8, 8, 0, -14, 7, 0, -11, 7, 0, -8, 7, 0, -5, 7, 0, -2, 7,
845 0, 1, 7, 0, 4, 7, 0, 7, 7, 0, 10, 7, 0, -13, 5, 0, -10, 5, 0, -7,
846 5, 0, -4, 5, 0, -1, 5, 0, 2, 5, 0, 5, 5, 0, 8, 5, 0, 11, 5, 0,
847 -14, 4, 0, -11, 4, 0, -8, 4, 0, -5, 4, 0, -2, 4, 0, 1, 4, 0, 4, 4,
848 0, 7, 4, 0, 10, 4, 0, -13, 2, 0, -10, 2, 0, -7, 2, 0, -4, 2, 0,
849 -1, 2, 0, 2, 2, 0, 5, 2, 0, 8, 2, 0, 11, 2, 0, -11, 1, 0, -8, 1,
850 0, -5, 1, 0, -2, 1, 0, 1, 1, 0, 4, 1, 0, 7, 1, 0, 10, 1, 0, 13, 1,
851 0, -10, -1, 0, -7, -1, 0, -4, -1, 0, -1, -1, 0, 2, -1, 0, 5, -1,
852 0, 8, -1, 0, 11, -1, 0, 14, -1, 0, -11, -2, 0, -8, -2, 0, -5, -2,
853 0, -2, -2, 0, 1, -2, 0, 4, -2, 0, 7, -2, 0, 10, -2, 0, 13, -2, 0,
854 -10, -4, 0, -7, -4, 0, -4, -4, 0, -1, -4, 0, 2, -4, 0, 5, -4, 0,
855 8, -4, 0, 11, -4, 0, 14, -4, 0, -8, -5, 0, -5, -5, 0, -2, -5, 0,
856 1, -5, 0, 4, -5, 0, 7, -5, 0, 10, -5, 0, 13, -5, 0, 16, -5, 0, -7,
857 -7, 0, -4, -7, 0, -1, -7, 0, 2, -7, 0, 5, -7, 0, 8, -7, 0, 11, -7,
858 0, 14, -7, 0, 17, -7, 0, -8, -8, 0, -5, -8, 0, -2, -8, 0, 1, -8,
859 0, 4, -8, 0, 7, -8, 0, 10, -8, 0, 13, -8, 0, 16, -8, 0, -7, -10,
860 0, -4, -10, 0, -1, -10, 0, 2, -10, 0, 5, -10, 0, 8, -10, 0, 11,
861 -10, 0, 14, -10, 0, 17, -10, 0, -5, -11, 0, -2, -11, 0, 1, -11, 0,
862 4, -11, 0, 7, -11, 0, 10, -11, 0, 13, -11, 0, 16, -11, 0, -1, -13,
863 0, 2, -13, 0, 5, -13, 0, 8, -13, 0, 11, -13, 0, 14, -13, 0, 1,
864 -14, 0, 4, -14, 0, 7, -14, 0, 10, -14, 0, 13, -14, 0, 5, -16, 0,
865 8, -16, 0, 11, -16, 0, 7, -17, 0, 10, -17, 0]).reshape((162, 3)) / 27.0
867cc162_1x1 = np.array([
868 -8, -16, 0, -10, -14, 0, -7, -14, 0, -4, -14,
869 0, -11, -13, 0, -8, -13, 0, -5, -13, 0, -2, -13, 0, -13, -11, 0,
870 -10, -11, 0, -7, -11, 0, -4, -11, 0, -1, -11, 0, 2, -11, 0, -14,
871 -10, 0, -11, -10, 0, -8, -10, 0, -5, -10, 0, -2, -10, 0, 1, -10,
872 0, 4, -10, 0, -16, -8, 0, -13, -8, 0, -10, -8, 0, -7, -8, 0, -4,
873 -8, 0, -1, -8, 0, 2, -8, 0, 5, -8, 0, 8, -8, 0, -14, -7, 0, -11,
874 -7, 0, -8, -7, 0, -5, -7, 0, -2, -7, 0, 1, -7, 0, 4, -7, 0, 7, -7,
875 0, 10, -7, 0, -13, -5, 0, -10, -5, 0, -7, -5, 0, -4, -5, 0, -1,
876 -5, 0, 2, -5, 0, 5, -5, 0, 8, -5, 0, 11, -5, 0, -14, -4, 0, -11,
877 -4, 0, -8, -4, 0, -5, -4, 0, -2, -4, 0, 1, -4, 0, 4, -4, 0, 7, -4,
878 0, 10, -4, 0, -13, -2, 0, -10, -2, 0, -7, -2, 0, -4, -2, 0, -1,
879 -2, 0, 2, -2, 0, 5, -2, 0, 8, -2, 0, 11, -2, 0, -11, -1, 0, -8,
880 -1, 0, -5, -1, 0, -2, -1, 0, 1, -1, 0, 4, -1, 0, 7, -1, 0, 10, -1,
881 0, 13, -1, 0, -10, 1, 0, -7, 1, 0, -4, 1, 0, -1, 1, 0, 2, 1, 0, 5,
882 1, 0, 8, 1, 0, 11, 1, 0, 14, 1, 0, -11, 2, 0, -8, 2, 0, -5, 2, 0,
883 -2, 2, 0, 1, 2, 0, 4, 2, 0, 7, 2, 0, 10, 2, 0, 13, 2, 0, -10, 4,
884 0, -7, 4, 0, -4, 4, 0, -1, 4, 0, 2, 4, 0, 5, 4, 0, 8, 4, 0, 11, 4,
885 0, 14, 4, 0, -8, 5, 0, -5, 5, 0, -2, 5, 0, 1, 5, 0, 4, 5, 0, 7, 5,
886 0, 10, 5, 0, 13, 5, 0, 16, 5, 0, -7, 7, 0, -4, 7, 0, -1, 7, 0, 2,
887 7, 0, 5, 7, 0, 8, 7, 0, 11, 7, 0, 14, 7, 0, 17, 7, 0, -8, 8, 0,
888 -5, 8, 0, -2, 8, 0, 1, 8, 0, 4, 8, 0, 7, 8, 0, 10, 8, 0, 13, 8, 0,
889 16, 8, 0, -7, 10, 0, -4, 10, 0, -1, 10, 0, 2, 10, 0, 5, 10, 0, 8,
890 10, 0, 11, 10, 0, 14, 10, 0, 17, 10, 0, -5, 11, 0, -2, 11, 0, 1,
891 11, 0, 4, 11, 0, 7, 11, 0, 10, 11, 0, 13, 11, 0, 16, 11, 0, -1,
892 13, 0, 2, 13, 0, 5, 13, 0, 8, 13, 0, 11, 13, 0, 14, 13, 0, 1, 14,
893 0, 4, 14, 0, 7, 14, 0, 10, 14, 0, 13, 14, 0, 5, 16, 0, 8, 16, 0,
894 11, 16, 0, 7, 17, 0, 10, 17, 0]).reshape((162, 3)) / 27.0
897# The following is a list of the critical points in the 1st Brillouin zone
898# for some typical crystal structures following the conventions of Setyawan
899# and Curtarolo [https://doi.org/10.1016/j.commatsci.2010.05.010].
900#
901# In units of the reciprocal basis vectors.
902#
903# See http://en.wikipedia.org/wiki/Brillouin_zone
904sc_special_points = {
905 'cubic': {'G': [0, 0, 0],
906 'M': [1 / 2, 1 / 2, 0],
907 'R': [1 / 2, 1 / 2, 1 / 2],
908 'X': [0, 1 / 2, 0]},
909 'fcc': {'G': [0, 0, 0],
910 'K': [3 / 8, 3 / 8, 3 / 4],
911 'L': [1 / 2, 1 / 2, 1 / 2],
912 'U': [5 / 8, 1 / 4, 5 / 8],
913 'W': [1 / 2, 1 / 4, 3 / 4],
914 'X': [1 / 2, 0, 1 / 2]},
915 'bcc': {'G': [0, 0, 0],
916 'H': [1 / 2, -1 / 2, 1 / 2],
917 'P': [1 / 4, 1 / 4, 1 / 4],
918 'N': [0, 0, 1 / 2]},
919 'tetragonal': {'G': [0, 0, 0],
920 'A': [1 / 2, 1 / 2, 1 / 2],
921 'M': [1 / 2, 1 / 2, 0],
922 'R': [0, 1 / 2, 1 / 2],
923 'X': [0, 1 / 2, 0],
924 'Z': [0, 0, 1 / 2]},
925 'orthorhombic': {'G': [0, 0, 0],
926 'R': [1 / 2, 1 / 2, 1 / 2],
927 'S': [1 / 2, 1 / 2, 0],
928 'T': [0, 1 / 2, 1 / 2],
929 'U': [1 / 2, 0, 1 / 2],
930 'X': [1 / 2, 0, 0],
931 'Y': [0, 1 / 2, 0],
932 'Z': [0, 0, 1 / 2]},
933 'hexagonal': {'G': [0, 0, 0],
934 'A': [0, 0, 1 / 2],
935 'H': [1 / 3, 1 / 3, 1 / 2],
936 'K': [1 / 3, 1 / 3, 0],
937 'L': [1 / 2, 0, 1 / 2],
938 'M': [1 / 2, 0, 0]}}
941# Old version of dictionary kept for backwards compatibility.
942# Not for ordinary use.
943ibz_points = {'cubic': {'Gamma': [0, 0, 0],
944 'X': [0, 0 / 2, 1 / 2],
945 'R': [1 / 2, 1 / 2, 1 / 2],
946 'M': [0 / 2, 1 / 2, 1 / 2]},
947 'fcc': {'Gamma': [0, 0, 0],
948 'X': [1 / 2, 0, 1 / 2],
949 'W': [1 / 2, 1 / 4, 3 / 4],
950 'K': [3 / 8, 3 / 8, 3 / 4],
951 'U': [5 / 8, 1 / 4, 5 / 8],
952 'L': [1 / 2, 1 / 2, 1 / 2]},
953 'bcc': {'Gamma': [0, 0, 0],
954 'H': [1 / 2, -1 / 2, 1 / 2],
955 'N': [0, 0, 1 / 2],
956 'P': [1 / 4, 1 / 4, 1 / 4]},
957 'hexagonal': {'Gamma': [0, 0, 0],
958 'M': [0, 1 / 2, 0],
959 'K': [-1 / 3, 1 / 3, 0],
960 'A': [0, 0, 1 / 2],
961 'L': [0, 1 / 2, 1 / 2],
962 'H': [-1 / 3, 1 / 3, 1 / 2]},
963 'tetragonal': {'Gamma': [0, 0, 0],
964 'X': [1 / 2, 0, 0],
965 'M': [1 / 2, 1 / 2, 0],
966 'Z': [0, 0, 1 / 2],
967 'R': [1 / 2, 0, 1 / 2],
968 'A': [1 / 2, 1 / 2, 1 / 2]},
969 'orthorhombic': {'Gamma': [0, 0, 0],
970 'R': [1 / 2, 1 / 2, 1 / 2],
971 'S': [1 / 2, 1 / 2, 0],
972 'T': [0, 1 / 2, 1 / 2],
973 'U': [1 / 2, 0, 1 / 2],
974 'X': [1 / 2, 0, 0],
975 'Y': [0, 1 / 2, 0],
976 'Z': [0, 0, 1 / 2]}}