Coverage for ase / lattice / __init__.py: 97.13%
766 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 08:22 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 08:22 +0000
1# fmt: off
3from abc import ABC, abstractmethod
4from dataclasses import dataclass
5from functools import cached_property
7import numpy as np
9from ase.cell import Cell
10from ase.dft.kpoints import BandPath, parse_path_string, sc_special_points
11from ase.utils import pbc2pbc
13_degrees = np.pi / 180
16def lattice_attr(name):
17 def getter(self):
18 try:
19 return self._parameters[name]
20 except KeyError:
21 raise AttributeError(name) from None
23 getter.__name__ = name
24 return property(getter)
27class BravaisLattice(ABC):
28 """Represent Bravais lattices and data related to the Brillouin zone.
30 There are 14 3D Bravais classes: CUB, FCC, BCC, ..., and TRI, and
31 five 2D classes.
33 Each class stores basic static information:
35 >>> from ase.lattice import FCC, MCL
36 >>> FCC.name
37 'FCC'
38 >>> FCC.longname
39 'face-centred cubic'
40 >>> FCC.pearson_symbol
41 'cF'
42 >>> MCL.parameters
43 ('a', 'b', 'c', 'alpha')
45 Each class can be instantiated with the specific lattice parameters
46 that apply to that lattice:
48 >>> MCL(3, 4, 5, 80)
49 MCL(a=3, b=4, c=5, alpha=80)
51 """
52 # These parameters can be set by the @bravais decorator for a subclass.
53 # (We could also use metaclasses to do this, but that's more abstract)
54 #
55 # We have to fight mypy so now we actually set initial values that are
56 # not None, unfortunately.
57 name: str = '' # e.g. 'CUB', 'BCT', 'ORCF', ...
58 longname: str = '' # e.g. 'cubic', 'body-centred tetragonal', ...
59 parameters: tuple = tuple() # e.g. ('a', 'c')
60 variants: dict | None = None # e.g. {'BCT1': <variant object>,
61 # 'BCT2': <variant object>}
62 ndim: int = 0
63 conventional_cls: str | None = None
65 def __init__(self, **kwargs):
66 p = {}
67 eps = kwargs.pop('eps', 2e-4)
68 for k, v in kwargs.items():
69 p[k] = float(v)
70 assert set(p) == set(self.parameters)
71 self._parameters = p
72 self._eps = eps
74 if len(self.variants) == 1:
75 # If there's only one it has the same name as the lattice type
76 self._variant = self.variants[self.name]
77 else:
78 name = self._variant_name(**self._parameters)
79 self._variant = self.variants[name]
81 @property
82 def variant(self) -> str:
83 """Return name of lattice variant.
85 >>> from ase.lattice import BCT
87 >>> BCT(3, 5).variant
88 'BCT2'
89 """
90 return self._variant.name
92 def vars(self) -> dict[str, float]:
93 """Get parameter names and values of this lattice as a dictionary."""
94 return dict(self._parameters)
96 def conventional(self) -> 'BravaisLattice':
97 """Get the conventional cell corresponding to this lattice."""
98 cls = bravais_lattices[self.conventional_cls]
99 return cls(**self._parameters)
101 def tocell(self) -> Cell:
102 """Return this lattice as a :class:`~ase.cell.Cell` object."""
103 cell = self._cell(**self._parameters)
104 return Cell(cell)
106 def cellpar(self) -> np.ndarray:
107 """Get cell lengths and angles as array of length 6.
109 See :func:`ase.geometry.Cell.cellpar`."""
110 # (Just a brute-force implementation)
111 cell = self.tocell()
112 return cell.cellpar()
114 @property
115 def special_path(self) -> str:
116 """Get default special k-point path for this lattice as a string.
118 >>> BCT(3, 5).special_path
119 'GXYSGZS1NPY1Z,XP'
120 """
121 return self._variant.special_path
123 @property
124 def special_point_names(self) -> list[str]:
125 """Return all special point names as a list of strings.
127 >>> from ase.lattice import BCT
129 >>> BCT(3, 5).special_point_names
130 ['G', 'N', 'P', 'S', 'S1', 'X', 'Y', 'Y1', 'Z']
131 """
132 labels = parse_path_string(self._variant.special_point_names)
133 assert len(labels) == 1 # list of lists
134 return labels[0]
136 def get_special_points_array(self) -> np.ndarray:
137 """Return all special points for this lattice as an array.
139 Ordering is consistent with special_point_names."""
140 if self._variant.special_points is not None:
141 # Fixed dictionary of special points
142 d = self.get_special_points()
143 labels = self.special_point_names
144 assert len(d) == len(labels)
145 points = np.empty((len(d), 3))
146 for i, label in enumerate(labels):
147 points[i] = d[label]
148 return points
150 # Special points depend on lattice parameters:
151 points = self._special_points(variant=self._variant,
152 **self._parameters)
153 assert len(points) == len(self.special_point_names)
154 return np.array(points)
156 def get_special_points(self) -> dict[str, np.ndarray]:
157 """Return a dictionary of named special k-points for this lattice."""
158 if self._variant.special_points is not None:
159 return self._variant.special_points
161 labels = self.special_point_names
162 points = self.get_special_points_array()
164 return dict(zip(labels, points))
166 def plot_bz(self, path=None, special_points=None, **plotkwargs):
167 """Plot the reciprocal cell and default bandpath."""
168 # Create a generic bandpath (no interpolated kpoints):
169 bandpath = self.bandpath(path=path, special_points=special_points,
170 npoints=0)
171 return bandpath.plot(dimension=self.ndim, **plotkwargs)
173 def bandpath(self, path=None, npoints=None, special_points=None,
174 density=None) -> BandPath:
175 """Return a :class:`~ase.dft.kpoints.BandPath` for this lattice.
177 See :meth:`ase.cell.Cell.bandpath` for description of parameters.
179 >>> from ase.lattice import BCT
181 >>> BCT(3, 5).bandpath()
182 BandPath(path='GXYSGZS1NPY1Z,XP', cell=[3x3], \
183special_points={GNPSS1XYY1Z}, kpts=[51x3])
185 .. note:: This produces the standard band path following AFlow
186 conventions. If your cell does not follow this convention,
187 you will need :meth:`ase.cell.Cell.bandpath` instead or
188 the kpoints may not correspond to your particular cell.
190 """
191 if special_points is None:
192 special_points = self.get_special_points()
194 if path is None:
195 path = self._variant.special_path
196 elif not isinstance(path, str):
197 from ase.dft.kpoints import resolve_custom_points
198 path, special_points = resolve_custom_points(path,
199 special_points,
200 self._eps)
202 cell = self.tocell()
203 bandpath = BandPath(cell=cell, path=path,
204 special_points=special_points)
205 return bandpath.interpolate(npoints=npoints, density=density)
207 @abstractmethod
208 def _cell(self, **kwargs):
209 """Return a Cell object from this Bravais lattice.
211 Arguments are the dictionary of Bravais parameters."""
213 def _special_points(self, **kwargs):
214 """Return the special point coordinates as an npoints x 3 sequence.
216 Subclasses typically return a nested list.
218 Ordering must be same as kpoint labels.
220 Arguments are the dictionary of Bravais parameters and the variant."""
221 raise NotImplementedError
223 def _variant_name(self, **kwargs):
224 """Return the name (e.g. 'ORCF3') of variant.
226 Arguments will be the dictionary of Bravais parameters."""
227 raise NotImplementedError
229 def __format__(self, spec):
230 tokens = []
231 if not spec:
232 spec = '.6g'
233 template = f'{{}}={{:{spec}}}'
235 for name in self.parameters:
236 value = self._parameters[name]
237 tokens.append(template.format(name, value))
238 return '{}({})'.format(self.name, ', '.join(tokens))
240 def __str__(self) -> str:
241 return self.__format__('')
243 def __repr__(self) -> str:
244 return self.__format__('.20g')
246 def description(self) -> str:
247 """Return complete description of lattice and Brillouin zone."""
248 points = self.get_special_points()
249 labels = self.special_point_names
251 coordstring = '\n'.join([' {:2s} {:7.4f} {:7.4f} {:7.4f}'
252 .format(label, *points[label])
253 for label in labels])
255 string = """\
256{repr}
257 {variant}
258 Special point coordinates:
259{special_points}
260""".format(repr=str(self),
261 variant=self._variant,
262 special_points=coordstring)
263 return string
265 @classmethod
266 def type_description(cls):
267 """Return complete description of this Bravais lattice type."""
268 desc = """\
269Lattice name: {name}
270 Long name: {longname}
271 Parameters: {parameters}
272""".format(**vars(cls))
274 chunks = [desc]
275 for name in cls.variant_names:
276 var = cls.variants[name]
277 txt = str(var)
278 lines = [' ' + L for L in txt.splitlines()]
279 lines.append('')
280 chunks.extend(lines)
282 return '\n'.join(chunks)
285class Variant:
286 variant_desc = """\
287Variant name: {name}
288 Special point names: {special_point_names}
289 Default path: {special_path}
290"""
292 def __init__(self, name, special_point_names, special_path,
293 special_points=None):
294 self.name = name
295 self.special_point_names = special_point_names
296 self.special_path = special_path
297 if special_points is not None:
298 special_points = dict(special_points)
299 for key, value in special_points.items():
300 special_points[key] = np.array(value)
301 self.special_points = special_points
302 # XXX Should make special_points available as a single array as well
303 # (easier to transform that way)
305 def __str__(self) -> str:
306 return self.variant_desc.format(**vars(self))
309bravais_names = []
310bravais_lattices = {}
311bravais_classes = {}
314def bravaisclass(longname, crystal_family, lattice_system, pearson_symbol,
315 parameters, variants, ndim=3):
316 """Decorator for Bravais lattice classes.
318 This sets a number of class variables and processes the information
319 about different variants into a list of Variant objects."""
321 def decorate(cls):
322 btype = cls.__name__
323 cls.name = btype
324 cls.longname = longname
325 cls.crystal_family = crystal_family
326 cls.lattice_system = lattice_system
327 cls.pearson_symbol = pearson_symbol
328 cls.parameters = tuple(parameters)
329 cls.variant_names = []
330 cls.variants = {}
331 cls.ndim = ndim
333 for parameter in parameters:
334 setattr(cls, parameter, lattice_attr(parameter))
336 for [name, special_point_names, special_path,
337 special_points] in variants:
338 cls.variant_names.append(name)
339 cls.variants[name] = Variant(name, special_point_names,
340 special_path, special_points)
342 # Register in global list and dictionary
343 bravais_names.append(btype)
344 bravais_lattices[btype] = cls
345 bravais_classes[pearson_symbol] = cls
346 return cls
348 return decorate
351# Common mappings of primitive to conventional cells:
352_identity = np.identity(3, int)
353_fcc_map = np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]])
354_bcc_map = np.array([[-1, 1, 1], [1, -1, 1], [1, 1, -1]])
357class UnconventionalLattice(ValueError):
358 pass
361class Cubic(BravaisLattice):
362 """Abstract class for cubic lattices."""
363 conventional_cls = 'CUB'
365 def __init__(self, a):
366 super().__init__(a=a)
369@bravaisclass('primitive cubic', 'cubic', 'cubic', 'cP', 'a',
370 [['CUB', 'GXRM', 'GXMGRX,MR', sc_special_points['cubic']]])
371class CUB(Cubic):
372 conventional_cellmap = _identity
374 def _cell(self, a):
375 return a * np.eye(3)
378@bravaisclass('face-centred cubic', 'cubic', 'cubic', 'cF', 'a',
379 [['FCC', 'GKLUWX', 'GXWKGLUWLK,UX', sc_special_points['fcc']]])
380class FCC(Cubic):
381 conventional_cellmap = _bcc_map
383 def _cell(self, a):
384 return 0.5 * np.array([[0., a, a], [a, 0, a], [a, a, 0]])
387@bravaisclass('body-centred cubic', 'cubic', 'cubic', 'cI', 'a',
388 [['BCC', 'GHPN', 'GHNGPH,PN', sc_special_points['bcc']]])
389class BCC(Cubic):
390 conventional_cellmap = _fcc_map
392 def _cell(self, a):
393 return 0.5 * np.array([[-a, a, a], [a, -a, a], [a, a, -a]])
396@bravaisclass('primitive tetragonal', 'tetragonal', 'tetragonal', 'tP', 'ac',
397 [['TET', 'GAMRXZ', 'GXMGZRAZ,XR,MA',
398 sc_special_points['tetragonal']]])
399class TET(BravaisLattice):
400 conventional_cls = 'TET'
401 conventional_cellmap = _identity
403 def __init__(self, a, c):
404 super().__init__(a=a, c=c)
406 def _cell(self, a, c):
407 return np.diag(np.array([a, a, c]))
410# XXX in BCT2 we use S for Sigma.
411# Also in other places I think
412@bravaisclass('body-centred tetragonal', 'tetragonal', 'tetragonal', 'tI',
413 'ac',
414 [['BCT1', 'GMNPXZZ1', 'GXMGZPNZ1M,XP', None],
415 ['BCT2', 'GNPSS1XYY1Z', 'GXYSGZS1NPY1Z,XP', None]])
416class BCT(BravaisLattice):
417 conventional_cls = 'TET'
418 conventional_cellmap = _fcc_map
420 def __init__(self, a, c):
421 super().__init__(a=a, c=c)
423 def _cell(self, a, c):
424 return 0.5 * np.array([[-a, a, c], [a, -a, c], [a, a, -c]])
426 def _variant_name(self, a, c):
427 return 'BCT1' if c < a else 'BCT2'
429 def _special_points(self, a, c, variant):
430 a2 = a * a
431 c2 = c * c
433 assert variant.name in self.variants
435 if variant.name == 'BCT1':
436 eta = .25 * (1 + c2 / a2)
437 points = [[0, 0, 0],
438 [-.5, .5, .5],
439 [0., .5, 0.],
440 [.25, .25, .25],
441 [0., 0., .5],
442 [eta, eta, -eta],
443 [-eta, 1 - eta, eta]]
444 else:
445 eta = .25 * (1 + a2 / c2) # Not same eta as BCT1!
446 zeta = 0.5 * a2 / c2
447 points = [[0., .0, 0.],
448 [0., .5, 0.],
449 [.25, .25, .25],
450 [-eta, eta, eta],
451 [eta, 1 - eta, -eta],
452 [0., 0., .5],
453 [-zeta, zeta, .5],
454 [.5, .5, -zeta],
455 [.5, .5, -.5]]
456 return points
459def check_orc(a, b, c):
460 if not a < b < c:
461 raise UnconventionalLattice('Expected a < b < c, got {}, {}, {}'
462 .format(a, b, c))
465class Orthorhombic(BravaisLattice):
466 """Abstract class for orthorhombic types."""
468 def __init__(self, a, b, c):
469 check_orc(a, b, c)
470 super().__init__(a=a, b=b, c=c)
473@bravaisclass('primitive orthorhombic', 'orthorhombic', 'orthorhombic', 'oP',
474 'abc',
475 [['ORC', 'GRSTUXYZ', 'GXSYGZURTZ,YT,UX,SR',
476 sc_special_points['orthorhombic']]])
477class ORC(Orthorhombic):
478 conventional_cls = 'ORC'
479 conventional_cellmap = _identity
481 def _cell(self, a, b, c):
482 return np.diag([a, b, c]).astype(float)
485@bravaisclass('face-centred orthorhombic', 'orthorhombic', 'orthorhombic',
486 'oF', 'abc',
487 [['ORCF1', 'GAA1LTXX1YZ', 'GYTZGXA1Y,TX1,XAZ,LG', None],
488 ['ORCF2', 'GCC1DD1LHH1XYZ', 'GYCDXGZD1HC,C1Z,XH1,HY,LG', None],
489 ['ORCF3', 'GAA1LTXX1YZ', 'GYTZGXA1Y,XAZ,LG', None]])
490class ORCF(Orthorhombic):
491 conventional_cls = 'ORC'
492 conventional_cellmap = _bcc_map
494 def _cell(self, a, b, c):
495 return 0.5 * np.array([[0, b, c], [a, 0, c], [a, b, 0]])
497 def _special_points(self, a, b, c, variant):
498 a2 = a * a
499 b2 = b * b
500 c2 = c * c
501 xminus = 0.25 * (1 + a2 / b2 - a2 / c2)
502 xplus = 0.25 * (1 + a2 / b2 + a2 / c2)
504 if variant.name == 'ORCF1' or variant.name == 'ORCF3':
505 zeta = xminus
506 eta = xplus
508 points = [[0, 0, 0],
509 [.5, .5 + zeta, zeta],
510 [.5, .5 - zeta, 1 - zeta],
511 [.5, .5, .5],
512 [1., .5, .5],
513 [0., eta, eta],
514 [1., 1 - eta, 1 - eta],
515 [.5, 0, .5],
516 [.5, .5, 0]]
517 else:
518 assert variant.name == 'ORCF2'
519 phi = 0.25 * (1 + c2 / b2 - c2 / a2)
520 delta = 0.25 * (1 + b2 / a2 - b2 / c2)
521 eta = xminus
523 points = [[0, 0, 0],
524 [.5, .5 - eta, 1 - eta],
525 [.5, .5 + eta, eta],
526 [.5 - delta, .5, 1 - delta],
527 [.5 + delta, .5, delta],
528 [.5, .5, .5],
529 [1 - phi, .5 - phi, .5],
530 [phi, .5 + phi, .5],
531 [0., .5, .5],
532 [.5, 0., .5],
533 [.5, .5, 0.]]
535 return points
537 def _variant_name(self, a, b, c):
538 diff = 1.0 / (a * a) - 1.0 / (b * b) - 1.0 / (c * c)
539 if abs(diff) < self._eps:
540 return 'ORCF3'
541 return 'ORCF1' if diff > 0 else 'ORCF2'
544@bravaisclass('body-centred orthorhombic', 'orthorhombic', 'orthorhombic',
545 'oI', 'abc',
546 [['ORCI', 'GLL1L2RSTWXX1YY1Z', 'GXLTWRX1ZGYSW,L1Y,Y1Z', None]])
547class ORCI(Orthorhombic):
548 conventional_cls = 'ORC'
549 conventional_cellmap = _fcc_map
551 def _cell(self, a, b, c):
552 return 0.5 * np.array([[-a, b, c], [a, -b, c], [a, b, -c]])
554 def _special_points(self, a, b, c, variant):
555 a2 = a**2
556 b2 = b**2
557 c2 = c**2
559 zeta = .25 * (1 + a2 / c2)
560 eta = .25 * (1 + b2 / c2)
561 delta = .25 * (b2 - a2) / c2
562 mu = .25 * (a2 + b2) / c2
564 points = [[0., 0., 0.],
565 [-mu, mu, .5 - delta],
566 [mu, -mu, .5 + delta],
567 [.5 - delta, .5 + delta, -mu],
568 [0, .5, 0],
569 [.5, 0, 0],
570 [0., 0., .5],
571 [.25, .25, .25],
572 [-zeta, zeta, zeta],
573 [zeta, 1 - zeta, -zeta],
574 [eta, -eta, eta],
575 [1 - eta, eta, -eta],
576 [.5, .5, -.5]]
577 return points
580@bravaisclass('base-centred orthorhombic', 'orthorhombic', 'orthorhombic',
581 'oC', 'abc',
582 [['ORCC', 'GAA1RSTXX1YZ', 'GXSRAZGYX1A1TY,ZT', None]])
583class ORCC(BravaisLattice):
584 conventional_cls = 'ORC'
585 conventional_cellmap = np.array([[1, 1, 0], [-1, 1, 0], [0, 0, 1]])
587 def __init__(self, a, b, c):
588 # ORCC is the only ORCx lattice with a<b and not a<b<c
589 if a >= b:
590 raise UnconventionalLattice(f'Expected a < b, got a={a}, b={b}')
591 super().__init__(a=a, b=b, c=c)
593 def _cell(self, a, b, c):
594 return np.array([[0.5 * a, -0.5 * b, 0], [0.5 * a, 0.5 * b, 0],
595 [0, 0, c]])
597 def _special_points(self, a, b, c, variant):
598 zeta = .25 * (1 + a * a / (b * b))
599 points = [[0, 0, 0],
600 [zeta, zeta, .5],
601 [-zeta, 1 - zeta, .5],
602 [0, .5, .5],
603 [0, .5, 0],
604 [-.5, .5, .5],
605 [zeta, zeta, 0],
606 [-zeta, 1 - zeta, 0],
607 [-.5, .5, 0],
608 [0, 0, .5]]
609 return points
612@bravaisclass('primitive hexagonal', 'hexagonal', 'hexagonal', 'hP',
613 'ac',
614 [['HEX', 'GMKALH', 'GMKGALHA,LM,KH',
615 sc_special_points['hexagonal']]])
616class HEX(BravaisLattice):
617 conventional_cls = 'HEX'
618 conventional_cellmap = _identity
620 def __init__(self, a, c):
621 super().__init__(a=a, c=c)
623 def _cell(self, a, c):
624 x = 0.5 * np.sqrt(3)
625 return np.array([[0.5 * a, -x * a, 0], [0.5 * a, x * a, 0],
626 [0., 0., c]])
629@bravaisclass('primitive rhombohedral', 'hexagonal', 'rhombohedral', 'hR',
630 ('a', 'alpha'),
631 [['RHL1', 'GBB1FLL1PP1P2QXZ', 'GLB1,BZGX,QFP1Z,LP', None],
632 ['RHL2', 'GFLPP1QQ1Z', 'GPZQGFP1Q1LZ', None]])
633class RHL(BravaisLattice):
634 conventional_cls = 'RHL'
635 conventional_cellmap = _identity
637 def __init__(self, a, alpha):
638 if alpha >= 120:
639 raise UnconventionalLattice('Need alpha < 120 degrees, got {}'
640 .format(alpha))
641 super().__init__(a=a, alpha=alpha)
643 def _cell(self, a, alpha):
644 alpha *= np.pi / 180
645 acosa = a * np.cos(alpha)
646 acosa2 = a * np.cos(0.5 * alpha)
647 asina2 = a * np.sin(0.5 * alpha)
648 acosfrac = acosa / acosa2
649 xx = (1 - acosfrac**2)
650 assert xx > 0.0
651 return np.array([[acosa2, -asina2, 0], [acosa2, asina2, 0],
652 [a * acosfrac, 0, a * xx**0.5]])
654 def _variant_name(self, a, alpha):
655 return 'RHL1' if alpha < 90 else 'RHL2'
657 def _special_points(self, a, alpha, variant):
658 if variant.name == 'RHL1':
659 cosa = np.cos(alpha * _degrees)
660 eta = (1 + 4 * cosa) / (2 + 4 * cosa)
661 nu = .75 - 0.5 * eta
662 points = [[0, 0, 0],
663 [eta, .5, 1 - eta],
664 [.5, 1 - eta, eta - 1],
665 [.5, .5, 0],
666 [.5, 0, 0],
667 [0, 0, -.5],
668 [eta, nu, nu],
669 [1 - nu, 1 - nu, 1 - eta],
670 [nu, nu, eta - 1],
671 [1 - nu, nu, 0],
672 [nu, 0, -nu],
673 [.5, .5, .5]]
674 else:
675 eta = 1 / (2 * np.tan(alpha * _degrees / 2)**2)
676 nu = .75 - 0.5 * eta
677 points = [[0, 0, 0],
678 [.5, -.5, 0],
679 [.5, 0, 0],
680 [1 - nu, -nu, 1 - nu],
681 [nu, nu - 1, nu - 1],
682 [eta, eta, eta],
683 [1 - eta, -eta, -eta],
684 [.5, -.5, .5]]
685 return points
688def check_mcl(a, b, c, alpha):
689 if b > c or alpha >= 90:
690 raise UnconventionalLattice('Expected b <= c, alpha < 90; '
691 'got a={}, b={}, c={}, alpha={}'
692 .format(a, b, c, alpha))
695@bravaisclass('primitive monoclinic', 'monoclinic', 'monoclinic', 'mP',
696 ('a', 'b', 'c', 'alpha'),
697 [['MCL', 'GACDD1EHH1H2MM1M2XYY1Z', 'GYHCEM1AXH1,MDZ,YD', None]])
698class MCL(BravaisLattice):
699 conventional_cls = 'MCL'
700 conventional_cellmap = _identity
702 def __init__(self, a, b, c, alpha):
703 check_mcl(a, b, c, alpha)
704 super().__init__(a=a, b=b, c=c, alpha=alpha)
706 def _cell(self, a, b, c, alpha):
707 alpha *= _degrees
708 return np.array([[a, 0, 0], [0, b, 0],
709 [0, c * np.cos(alpha), c * np.sin(alpha)]])
711 def _special_points(self, a, b, c, alpha, variant):
712 cosa = np.cos(alpha * _degrees)
713 eta = (1 - b * cosa / c) / (2 * np.sin(alpha * _degrees)**2)
714 nu = .5 - eta * c * cosa / b
716 points = [[0, 0, 0],
717 [.5, .5, 0],
718 [0, .5, .5],
719 [.5, 0, .5],
720 [.5, 0, -.5],
721 [.5, .5, .5],
722 [0, eta, 1 - nu],
723 [0, 1 - eta, nu],
724 [0, eta, -nu],
725 [.5, eta, 1 - nu],
726 [.5, 1 - eta, nu],
727 [.5, eta, -nu],
728 [0, .5, 0],
729 [0, 0, .5],
730 [0, 0, -.5],
731 [.5, 0, 0]]
732 return points
734 def _variant_name(self, a, b, c, alpha):
735 check_mcl(a, b, c, alpha)
736 return 'MCL'
739@bravaisclass('base-centred monoclinic', 'monoclinic', 'monoclinic', 'mC',
740 ('a', 'b', 'c', 'alpha'),
741 [['MCLC1', 'GNN1FF1F2F3II1LMXX1X2YY1Z',
742 'GYFLI,I1ZF1,YX1,XGN,MG', None],
743 ['MCLC2', 'GNN1FF1F2F3II1LMXX1X2YY1Z',
744 'GYFLI,I1ZF1,NGM', None],
745 ['MCLC3', 'GFF1F2HH1H2IMNN1XYY1Y2Y3Z',
746 'GYFHZIF1,H1Y1XGN,MG', None],
747 ['MCLC4', 'GFF1F2HH1H2IMNN1XYY1Y2Y3Z',
748 'GYFHZI,H1Y1XGN,MG', None],
749 ['MCLC5', 'GFF1F2HH1H2II1LMNN1XYY1Y2Y3Z',
750 'GYFLI,I1ZHF1,H1Y1XGN,MG', None]])
751class MCLC(BravaisLattice):
752 conventional_cls = 'MCL'
753 conventional_cellmap = np.array([[1, -1, 0], [1, 1, 0], [0, 0, 1]])
755 def __init__(self, a, b, c, alpha):
756 check_mcl(a, b, c, alpha)
757 super().__init__(a=a, b=b, c=c, alpha=alpha)
759 def _cell(self, a, b, c, alpha):
760 alpha *= np.pi / 180
761 return np.array([[0.5 * a, 0.5 * b, 0], [-0.5 * a, 0.5 * b, 0],
762 [0, c * np.cos(alpha), c * np.sin(alpha)]])
764 def _variant_name(self, a, b, c, alpha):
765 # from ase.geometry.cell import mclc
766 # okay, this is a bit hacky
768 # We need the same parameters here as when determining the points.
769 # Right now we just repeat the code:
770 check_mcl(a, b, c, alpha)
772 a2 = a * a
773 b2 = b * b
774 cosa = np.cos(alpha * _degrees)
775 sina = np.sin(alpha * _degrees)
776 sina2 = sina**2
778 cell = self.tocell()
779 lengths_angles = Cell(cell.reciprocal()).cellpar()
781 kgamma = lengths_angles[-1]
783 eps = self._eps
784 # We should not compare angles in degrees versus lengths with
785 # the same precision.
786 if abs(kgamma - 90) < eps:
787 variant = 2
788 elif kgamma > 90:
789 variant = 1
790 elif kgamma < 90:
791 num = b * cosa / c + b2 * sina2 / a2
792 if abs(num - 1) < eps:
793 variant = 4
794 elif num < 1:
795 variant = 3
796 else:
797 variant = 5
798 variant = 'MCLC' + str(variant)
799 return variant
801 def _special_points(self, a, b, c, alpha, variant):
802 variant = int(variant.name[-1])
804 a2 = a * a
805 b2 = b * b
806 # c2 = c * c
807 cosa = np.cos(alpha * _degrees)
808 sina = np.sin(alpha * _degrees)
809 sina2 = sina**2
811 if variant == 1 or variant == 2:
812 zeta = (2 - b * cosa / c) / (4 * sina2)
813 eta = 0.5 + 2 * zeta * c * cosa / b
814 psi = .75 - a2 / (4 * b2 * sina * sina)
815 phi = psi + (.75 - psi) * b * cosa / c
817 points = [[0, 0, 0],
818 [.5, 0, 0],
819 [0, -.5, 0],
820 [1 - zeta, 1 - zeta, 1 - eta],
821 [zeta, zeta, eta],
822 [-zeta, -zeta, 1 - eta],
823 [1 - zeta, -zeta, 1 - eta],
824 [phi, 1 - phi, .5],
825 [1 - phi, phi - 1, .5],
826 [.5, .5, .5],
827 [.5, 0, .5],
828 [1 - psi, psi - 1, 0],
829 [psi, 1 - psi, 0],
830 [psi - 1, -psi, 0],
831 [.5, .5, 0],
832 [-.5, -.5, 0],
833 [0, 0, .5]]
834 elif variant == 3 or variant == 4:
835 mu = .25 * (1 + b2 / a2)
836 delta = b * c * cosa / (2 * a2)
837 zeta = mu - 0.25 + (1 - b * cosa / c) / (4 * sina2)
838 eta = 0.5 + 2 * zeta * c * cosa / b
839 phi = 1 + zeta - 2 * mu
840 psi = eta - 2 * delta
842 points = [[0, 0, 0],
843 [1 - phi, 1 - phi, 1 - psi],
844 [phi, phi - 1, psi],
845 [1 - phi, -phi, 1 - psi],
846 [zeta, zeta, eta],
847 [1 - zeta, -zeta, 1 - eta],
848 [-zeta, -zeta, 1 - eta],
849 [.5, -.5, .5],
850 [.5, 0, .5],
851 [.5, 0, 0],
852 [0, -.5, 0],
853 [.5, -.5, 0],
854 [mu, mu, delta],
855 [1 - mu, -mu, -delta],
856 [-mu, -mu, -delta],
857 [mu, mu - 1, delta],
858 [0, 0, .5]]
859 elif variant == 5:
860 zeta = .25 * (b2 / a2 + (1 - b * cosa / c) / sina2)
861 eta = 0.5 + 2 * zeta * c * cosa / b
862 mu = .5 * eta + b2 / (4 * a2) - b * c * cosa / (2 * a2)
863 nu = 2 * mu - zeta
864 omega = (4 * nu - 1 - b2 * sina2 / a2) * c / (2 * b * cosa)
865 delta = zeta * c * cosa / b + omega / 2 - .25
866 rho = 1 - zeta * a2 / b2
868 points = [[0, 0, 0],
869 [nu, nu, omega],
870 [1 - nu, 1 - nu, 1 - omega],
871 [nu, nu - 1, omega],
872 [zeta, zeta, eta],
873 [1 - zeta, -zeta, 1 - eta],
874 [-zeta, -zeta, 1 - eta],
875 [rho, 1 - rho, .5],
876 [1 - rho, rho - 1, .5],
877 [.5, .5, .5],
878 [.5, 0, .5],
879 [.5, 0, 0],
880 [0, -.5, 0],
881 [.5, -.5, 0],
882 [mu, mu, delta],
883 [1 - mu, -mu, -delta],
884 [-mu, -mu, -delta],
885 [mu, mu - 1, delta],
886 [0, 0, .5]]
888 return points
891tri_angles_explanation = """\
892Angles kalpha, kbeta and kgamma of TRI lattice must be 1) all greater \
893than 90 degrees with kgamma being the smallest, or 2) all smaller than \
89490 with kgamma being the largest, or 3) kgamma=90 being the \
895smallest of the three, or 4) kgamma=90 being the largest of the three. \
896Angles of reciprocal lattice are kalpha={}, kbeta={}, kgamma={}. \
897If you don't care, please use Cell.fromcellpar() instead."""
899# XXX labels, paths, are all the same.
902@bravaisclass('primitive triclinic', 'triclinic', 'triclinic', 'aP',
903 ('a', 'b', 'c', 'alpha', 'beta', 'gamma'),
904 [['TRI1a', 'GLMNRXYZ', 'XGY,LGZ,NGM,RG', None],
905 ['TRI2a', 'GLMNRXYZ', 'XGY,LGZ,NGM,RG', None],
906 ['TRI1b', 'GLMNRXYZ', 'XGY,LGZ,NGM,RG', None],
907 ['TRI2b', 'GLMNRXYZ', 'XGY,LGZ,NGM,RG', None]])
908class TRI(BravaisLattice):
909 conventional_cls = 'TRI'
910 conventional_cellmap = _identity
912 def __init__(self, a, b, c, alpha, beta, gamma):
913 super().__init__(a=a, b=b, c=c, alpha=alpha, beta=beta,
914 gamma=gamma)
916 def _cell(self, a, b, c, alpha, beta, gamma):
917 alpha, beta, gamma = np.array([alpha, beta, gamma])
918 singamma = np.sin(gamma * _degrees)
919 cosgamma = np.cos(gamma * _degrees)
920 cosbeta = np.cos(beta * _degrees)
921 cosalpha = np.cos(alpha * _degrees)
922 a3x = c * cosbeta
923 a3y = c / singamma * (cosalpha - cosbeta * cosgamma)
924 a3z = c / singamma * np.sqrt(singamma**2 - cosalpha**2 - cosbeta**2
925 + 2 * cosalpha * cosbeta * cosgamma)
926 return np.array([[a, 0, 0], [b * cosgamma, b * singamma, 0],
927 [a3x, a3y, a3z]])
929 def _variant_name(self, a, b, c, alpha, beta, gamma):
930 cell = Cell.new([a, b, c, alpha, beta, gamma])
931 icellpar = Cell(cell.reciprocal()).cellpar()
932 kangles = kalpha, kbeta, kgamma = icellpar[3:]
934 def raise_unconventional():
935 raise UnconventionalLattice(tri_angles_explanation
936 .format(*kangles))
938 eps = self._eps
939 if abs(kgamma - 90) < eps:
940 if kalpha > 90 and kbeta > 90:
941 var = '2a'
942 elif kalpha < 90 and kbeta < 90:
943 var = '2b'
944 else:
945 # Is this possible? Maybe due to epsilon
946 raise_unconventional()
947 elif all(kangles > 90):
948 if kgamma > min(kangles):
949 raise_unconventional()
950 var = '1a'
951 elif all(kangles < 90): # and kgamma > max(kalpha, kbeta):
952 if kgamma < max(kangles):
953 raise_unconventional()
954 var = '1b'
955 else:
956 raise_unconventional()
958 return 'TRI' + var
960 def _special_points(self, a, b, c, alpha, beta, gamma, variant):
961 # (None of the points actually depend on any parameters)
962 # (We should store the points openly on the variant objects)
963 if variant.name == 'TRI1a' or variant.name == 'TRI2a':
964 points = [[0., 0., 0.],
965 [.5, .5, 0],
966 [0, .5, .5],
967 [.5, 0, .5],
968 [.5, .5, .5],
969 [.5, 0, 0],
970 [0, .5, 0],
971 [0, 0, .5]]
972 else:
973 points = [[0, 0, 0],
974 [.5, -.5, 0],
975 [0, 0, .5],
976 [-.5, -.5, .5],
977 [0, -.5, .5],
978 [0, -0.5, 0],
979 [.5, 0, 0],
980 [-.5, 0, .5]]
982 return points
985def get_subset_points(names, points):
986 newpoints = {name: points[name] for name in names}
987 return newpoints
990@bravaisclass('primitive oblique', 'monoclinic', None, 'mp',
991 ('a', 'b', 'alpha'), [['OBL', 'GYHCH1X', 'GYHCH1XG', None]],
992 ndim=2)
993class OBL(BravaisLattice):
994 def __init__(self, a, b, alpha, **kwargs):
995 check_rect(a, b)
996 if alpha >= 90:
997 raise UnconventionalLattice(
998 f'Expected alpha < 90, got alpha={alpha}')
999 super().__init__(a=a, b=b, alpha=alpha, **kwargs)
1001 def _cell(self, a, b, alpha):
1002 cosa = np.cos(alpha * _degrees)
1003 sina = np.sin(alpha * _degrees)
1005 return np.array([[a, 0, 0],
1006 [b * cosa, b * sina, 0],
1007 [0., 0., 0.]])
1009 def _special_points(self, a, b, alpha, variant):
1010 cosa = np.cos(alpha * _degrees)
1011 eta = (1 - a * cosa / b) / (2 * np.sin(alpha * _degrees)**2)
1012 nu = .5 - eta * b * cosa / a
1014 points = [[0, 0, 0],
1015 [0, 0.5, 0],
1016 [eta, 1 - nu, 0],
1017 [.5, .5, 0],
1018 [1 - eta, nu, 0],
1019 [.5, 0, 0]]
1021 return points
1024@bravaisclass('primitive hexagonal', 'hexagonal', None, 'hp', 'a',
1025 [['HEX2D', 'GMK', 'GMKG',
1026 get_subset_points('GMK',
1027 sc_special_points['hexagonal'])]],
1028 ndim=2)
1029class HEX2D(BravaisLattice):
1030 def __init__(self, a, **kwargs):
1031 super().__init__(a=a, **kwargs)
1033 def _cell(self, a):
1034 x = 0.5 * np.sqrt(3)
1035 return np.array([[a, 0, 0],
1036 [-0.5 * a, x * a, 0],
1037 [0., 0., 0.]])
1040def check_rect(a, b):
1041 if a >= b:
1042 raise UnconventionalLattice(f'Expected a < b, got a={a}, b={b}')
1045@bravaisclass('primitive rectangular', 'orthorhombic', None, 'op', 'ab',
1046 [['RECT', 'GXSY', 'GXSYGS',
1047 get_subset_points('GXSY',
1048 sc_special_points['orthorhombic'])]],
1049 ndim=2)
1050class RECT(BravaisLattice):
1051 def __init__(self, a, b, **kwargs):
1052 check_rect(a, b)
1053 super().__init__(a=a, b=b, **kwargs)
1055 def _cell(self, a, b):
1056 return np.array([[a, 0, 0],
1057 [0, b, 0],
1058 [0, 0, 0.]])
1061@bravaisclass('centred rectangular', 'orthorhombic', None, 'oc',
1062 ('a', 'alpha'), [['CRECT', 'GXA1Y', 'GXA1YG', None]], ndim=2)
1063class CRECT(BravaisLattice):
1064 def __init__(self, a, alpha, **kwargs):
1065 # It would probably be better to define the CRECT cell
1066 # by (a, b) rather than (a, alpha). Then we can require a < b
1067 # like in ordinary RECT.
1068 #
1069 # In 3D, all lattices in the same family generally take
1070 # identical parameters.
1071 if alpha >= 90:
1072 raise UnconventionalLattice(
1073 f'Expected alpha < 90. Got alpha={alpha}')
1074 super().__init__(a=a, alpha=alpha, **kwargs)
1076 def _cell(self, a, alpha):
1077 x = np.cos(alpha * _degrees)
1078 y = np.sin(alpha * _degrees)
1079 return np.array([[a, 0, 0],
1080 [a * x, a * y, 0],
1081 [0, 0, 0.]])
1083 def _special_points(self, a, alpha, variant):
1084 sina2 = np.sin(alpha / 2 * _degrees)**2
1085 sina = np.sin(alpha * _degrees)**2
1086 eta = sina2 / sina
1087 cosa = np.cos(alpha * _degrees)
1088 xi = eta * cosa
1090 points = [[0, 0, 0],
1091 [eta, - eta, 0],
1092 [0.5 + xi, 0.5 - xi, 0],
1093 [0.5, 0.5, 0]]
1095 return points
1098@bravaisclass('primitive square', 'tetragonal', None, 'tp', ('a',),
1099 [['SQR', 'GMX', 'MGXM',
1100 get_subset_points('GMX', sc_special_points['tetragonal'])]],
1101 ndim=2)
1102class SQR(BravaisLattice):
1103 def __init__(self, a, **kwargs):
1104 super().__init__(a=a, **kwargs)
1106 def _cell(self, a):
1107 return np.array([[a, 0, 0],
1108 [0, a, 0],
1109 [0, 0, 0.]])
1112@bravaisclass('primitive line', 'line', None, '?', ('a',),
1113 [['LINE', 'GX', 'GX', {'G': [0, 0, 0], 'X': [0.5, 0, 0]}]],
1114 ndim=1)
1115class LINE(BravaisLattice):
1116 def __init__(self, a, **kwargs):
1117 super().__init__(a=a, **kwargs)
1119 def _cell(self, a):
1120 return np.array([[a, 0.0, 0.0],
1121 [0.0, 0.0, 0.0],
1122 [0.0, 0.0, 0.0]])
1125def celldiff(cell1, cell2):
1126 """Return a unitless measure of the difference between two cells."""
1127 cell1 = Cell.ascell(cell1).complete()
1128 cell2 = Cell.ascell(cell2).complete()
1129 v1v2 = cell1.volume * cell2.volume
1130 if v1v2 < 1e-10:
1131 # (Proposed cell may be linearly dependent)
1132 return np.inf
1134 scale = v1v2**(-1. / 3.) # --> 1/Ang^2
1135 x1 = cell1 @ cell1.T
1136 x2 = cell2 @ cell2.T
1137 dev = scale * np.abs(x2 - x1).max()
1138 return dev
1141def get_lattice_from_canonical_cell(cell, eps=2e-4):
1142 """Return a Bravais lattice representing the given cell.
1144 This works only for cells that are derived from the standard form
1145 (as generated by lat.tocell()) or rotations thereof.
1147 If the given cell does not resemble the known form of a Bravais
1148 lattice, raise RuntimeError."""
1149 return NormalizedLatticeMatcher(cell, eps).match()
1152class LatticeMatcher:
1153 def __init__(self, cell, pbc=None, *, eps, niggli_eps=None):
1154 self.orig_cell = cell
1155 if pbc is None:
1156 pbc = cell.mask()
1157 self.pbc = cell.any(1) & pbc2pbc(pbc)
1158 self.cell = cell.uncomplete(pbc)
1159 self.eps = eps
1160 if niggli_eps is None:
1161 niggli_eps = eps
1162 self.niggli_cell, self.niggli_op = self.cell.niggli_reduce(
1163 eps=niggli_eps)
1165 # We tabulate the cell's Niggli-mapped versions so we don't need to
1166 # redo any work when the same Niggli-operation appears multiple times
1167 # in the table:
1168 self.memory = {}
1170 def match(self, latname, operations):
1171 matches = []
1173 for op_key in operations:
1174 checker_and_op = self.memory.get(op_key)
1175 if checker_and_op is None:
1176 # op_3x3 is the 3x3 form of the operation that maps
1177 # Niggli cell to AFlow form.
1178 op_3x3 = np.array(op_key).reshape(3, 3)
1179 candidate = Cell(np.linalg.inv(op_3x3.T) @ self.niggli_cell)
1180 checker = NormalizedLatticeMatcher(candidate, eps=self.eps)
1181 self.memory[op_key] = (checker, op_3x3)
1182 else:
1183 checker, op_3x3 = checker_and_op
1185 lat, error = checker.query(latname)
1186 if lat is None or error > self.eps:
1187 continue
1189 # This is the full operation encompassing
1190 # both Niggli reduction of user input and mapping the
1191 # Niggli reduced form to standard (AFlow) form.
1192 op = op_3x3 @ np.linalg.inv(self.niggli_op)
1193 matches.append(Match(lat, op, error))
1195 return matches
1198@dataclass
1199class Match:
1200 lat: BravaisLattice
1201 op: np.ndarray
1202 error: float
1204 @cached_property
1205 def orthogonality_defect(self) -> float:
1206 cell = self.lat.tocell().complete()
1207 return np.prod(cell.lengths()) / cell.volume
1210def identify_lattice(cell, eps=2e-4, *, pbc=True):
1211 """Find Bravais lattice representing this cell.
1213 Returns Bravais lattice object representing the cell along with
1214 an operation that, applied to the cell, yields the same lengths
1215 and angles as the Bravais lattice object."""
1216 from ase.geometry.bravais_type_engine import niggli_op_table
1218 matcher = LatticeMatcher(cell, pbc, eps=eps)
1220 # We loop through the most symmetric kinds (CUB etc.) and return
1221 # the first one we find:
1222 for latname in lattice_check_orders[matcher.cell.rank]:
1223 # There may be multiple Niggli operations that produce valid
1224 # lattices, at least for MCL. In that case we will pick the
1225 # one whose angle is closest to 90, but it means we cannot
1226 # just return the first one we find so we must remember them:
1227 matches = matcher.match(latname, niggli_op_table[latname])
1229 if not matches:
1230 continue # Move to next Bravais lattice
1232 best = min(matches, key=lambda match: match.orthogonality_defect)
1234 if matcher.cell.rank == 2 and best.op[2, 2] < 0:
1235 best.op = flip_2d_handedness(best.op)
1237 return best.lat, best.op
1239 raise RuntimeError('Failed to recognize lattice')
1242def flip_2d_handedness(op):
1243 # The 3x3 operation may flip the z axis, but then the x/y
1244 # components are necessarily also left-handed which
1245 # means a defacto left-handed 2D bandpath.
1246 #
1247 # We repair this by applying an operation that unflips the
1248 # z axis and interchanges x/y:
1249 repair_op = np.array([[0, 1, 0], [1, 0, 0], [0, 0, -1]])
1250 return repair_op @ op
1253# Map of number of dimensions to order in which to check lattices.
1254# We check most symmetric lattices first, so we can terminate early
1255# when a match is found.
1256#
1257# The check order is slightly different than elsewhere listed order,
1258# as we need to check HEX/RHL before the ORCx family.
1259lattice_check_orders = {
1260 1: ['LINE'],
1261 2: ['SQR', 'RECT', 'HEX2D', 'CRECT', 'OBL'],
1262 3: ['CUB', 'FCC', 'BCC', 'TET', 'BCT', 'HEX', 'RHL',
1263 'ORC', 'ORCF', 'ORCI', 'ORCC', 'MCL', 'MCLC', 'TRI']}
1266class NormalizedLatticeMatcher:
1267 # This class checks that a candidate cell matches the normalized
1268 # form of a Bravais lattice. It's used internally by the LatticeMatcher.
1270 def __init__(self, cell, eps=2e-4):
1271 """Generate Bravais lattices that look (or not) like the given cell.
1273 The cell must be reduced to canonical form, i.e., it must
1274 be possible to produce a cell with the same lengths and angles
1275 by directly through one of the Bravais lattice classes.
1277 Generally for internal use (this module).
1279 For each of the 14 Bravais lattices, this object can produce
1280 a lattice object which represents the same cell, or None if
1281 the tolerance eps is not met."""
1282 self.cell = cell
1283 self.eps = eps
1285 self.cellpar = cell.cellpar()
1286 self.lengths = self.A, self.B, self.C = self.cellpar[:3]
1287 self.angles = self.cellpar[3:]
1289 # Use a 'neutral' length for checking cubic lattices
1290 self.A0 = self.lengths.mean()
1292 # Vector of the diagonal and then off-diagonal dot products:
1293 # [a1 · a1, a2 · a2, a3 · a3, a2 · a3, a3 · a1, a1 · a2]
1294 self.prods = (cell @ cell.T).flat[[0, 4, 8, 5, 2, 1]]
1296 def _check(self, latcls, *args):
1297 if any(arg <= 0 for arg in args):
1298 return None
1300 try:
1301 return latcls(*args)
1302 except UnconventionalLattice:
1303 return None
1305 def match(self):
1306 """Match cell against all lattices, returning most symmetric match.
1308 Returns the lattice object. Raises RuntimeError on failure."""
1309 for name in lattice_check_orders[self.cell.rank]:
1310 lat, err = self.query(name)
1311 if lat and err < self.eps:
1312 return lat
1314 raise RuntimeError('Could not find lattice type for cell '
1315 'with lengths and angles {}'
1316 .format(self.cell.cellpar().tolist()))
1318 def query(self, latname):
1319 """Match cell against named Bravais lattice.
1321 Return lattice object on success, None on failure."""
1322 meth = getattr(self, latname)
1324 lat = meth()
1325 if lat is None:
1326 return None, None
1328 newcell = lat.tocell()
1329 err = celldiff(self.cell, newcell)
1330 return lat, err
1332 def LINE(self):
1333 return self._check(LINE, self.lengths[0])
1335 def SQR(self):
1336 return self._check(SQR, self.lengths[0])
1338 def RECT(self):
1339 return self._check(RECT, *self.lengths[:2])
1341 def CRECT(self):
1342 return self._check(CRECT, self.lengths[0], self.angles[2])
1344 def HEX2D(self):
1345 return self._check(HEX2D, self.lengths[0])
1347 def OBL(self):
1348 return self._check(OBL, *self.lengths[:2], self.angles[2])
1350 def CUB(self):
1351 # These methods (CUB, FCC, ...) all return a lattice object if
1352 # it matches, else None.
1353 return self._check(CUB, self.A0)
1355 def FCC(self):
1356 return self._check(FCC, np.sqrt(2) * self.A0)
1358 def BCC(self):
1359 return self._check(BCC, 2.0 * self.A0 / np.sqrt(3))
1361 def TET(self):
1362 return self._check(TET, self.A, self.C)
1364 def _bct_orci_lengths(self):
1365 # Coordinate-system independent relation for BCT and ORCI
1366 # standard cells:
1367 # a1 · a1 + a2 · a3 == a² / 2
1368 # a2 · a2 + a3 · a1 == a² / 2 (BCT)
1369 # == b² / 2 (ORCI)
1370 # a3 · a3 + a1 · a2 == c² / 2
1371 # We use these to get a, b, and c in those cases.
1372 prods = self.prods
1373 lengthsqr = 2.0 * (prods[:3] + prods[3:])
1374 if any(lengthsqr < 0):
1375 return None
1376 return np.sqrt(lengthsqr)
1378 def BCT(self):
1379 lengths = self._bct_orci_lengths()
1380 if lengths is None:
1381 return None
1382 return self._check(BCT, lengths[0], lengths[2])
1384 def HEX(self):
1385 return self._check(HEX, self.A, self.C)
1387 def RHL(self):
1388 return self._check(RHL, self.A, self.angles[0])
1390 def ORC(self):
1391 return self._check(ORC, *self.lengths)
1393 def ORCF(self):
1394 # ORCF standard cell:
1395 # a2 · a3 = a²/4
1396 # a3 · a1 = b²/4
1397 # a1 · a2 = c²/4
1398 prods = self.prods
1399 if all(prods[3:] > 0):
1400 orcf_abc = 2 * np.sqrt(prods[3:])
1401 return self._check(ORCF, *orcf_abc)
1403 def ORCI(self):
1404 lengths = self._bct_orci_lengths()
1405 if lengths is None:
1406 return None
1407 return self._check(ORCI, *lengths)
1409 def _orcc_ab(self):
1410 # ORCC: a1 · a1 + a2 · a3 = a²/2
1411 # a2 · a2 - a2 · a3 = b²/2
1412 prods = self.prods
1413 orcc_sqr_ab = np.empty(2)
1414 orcc_sqr_ab[0] = 2.0 * (prods[0] + prods[5])
1415 orcc_sqr_ab[1] = 2.0 * (prods[1] - prods[5])
1416 if all(orcc_sqr_ab > 0):
1417 return np.sqrt(orcc_sqr_ab)
1419 def ORCC(self):
1420 orcc_lengths_ab = self._orcc_ab()
1421 if orcc_lengths_ab is None:
1422 return None
1423 return self._check(ORCC, *orcc_lengths_ab, self.C)
1425 def MCL(self):
1426 return self._check(MCL, *self.lengths, self.angles[0])
1428 def MCLC(self):
1429 # MCLC is similar to ORCC:
1430 orcc_ab = self._orcc_ab()
1431 if orcc_ab is None:
1432 return None
1434 prods = self.prods
1435 C = self.C
1436 mclc_a, mclc_b = orcc_ab[::-1] # a, b reversed wrt. ORCC
1437 mclc_cosa = 2.0 * prods[3] / (mclc_b * C)
1438 if -1 < mclc_cosa < 1:
1439 mclc_alpha = np.arccos(mclc_cosa) * 180 / np.pi
1440 if mclc_b > C:
1441 # XXX Temporary fix for certain otherwise
1442 # unrecognizable lattices.
1443 #
1444 # This error could happen if the input lattice maps to
1445 # something just outside the domain of conventional
1446 # lattices (less than the tolerance). Our solution is to
1447 # propose a nearby conventional lattice instead, which
1448 # will then be accepted if it's close enough.
1449 mclc_b = 0.5 * (mclc_b + C)
1450 C = mclc_b
1451 return self._check(MCLC, mclc_a, mclc_b, C, mclc_alpha)
1453 def TRI(self):
1454 return self._check(TRI, *self.cellpar)
1457def match_to_lattice(cell, latname: str, pbc=None) -> list[Match]:
1458 """Match cell to a particular Bravais lattice.
1460 For a given input cell, attempt to represent it as a particular
1461 lattice, such as 'FCC', 'ORC', 'ORCI', etc. Return a list of
1462 :class:`ase.lattice.Match` objects which can be compared
1463 so as to get the best match according to metric tensor error
1464 with respect to the input cell or orthogonality defect.
1465 """
1466 from ase.geometry.bravais_type_engine import niggli_op_table
1468 cell = Cell.ascell(cell)
1469 matcher = LatticeMatcher(
1470 cell=cell,
1471 pbc=pbc,
1472 eps=1e6, # We want all candidates and apply eps in our own way
1473 niggli_eps=1e-5,
1474 )
1476 # We should probably sort the matches by "quality".
1477 # Quality must optimize both fitting error and
1478 # orthogonality defect, so some weighting scheme is necessary,
1479 # e.g. minimize one after filtering out the other.
1480 #
1481 # best = min(matches, key=lambda match: match.orthogonality_defect)
1482 #
1483 # Maybe we should take a key callable as argument.
1484 return matcher.match(latname, niggli_op_table[latname])
1487def all_variants():
1488 """For testing and examples; yield all variants of all lattices."""
1489 a, b, c = 3., 4., 5.
1490 alpha = 55.0
1491 yield CUB(a)
1492 yield FCC(a)
1493 yield BCC(a)
1494 yield TET(a, c)
1495 bct1 = BCT(2 * a, c)
1496 bct2 = BCT(a, c)
1497 assert bct1.variant == 'BCT1'
1498 assert bct2.variant == 'BCT2'
1500 yield bct1
1501 yield bct2
1503 yield ORC(a, b, c)
1505 a0 = np.sqrt(1.0 / (1 / b**2 + 1 / c**2))
1506 orcf1 = ORCF(0.5 * a0, b, c)
1507 orcf2 = ORCF(1.2 * a0, b, c)
1508 orcf3 = ORCF(a0, b, c)
1509 assert orcf1.variant == 'ORCF1'
1510 assert orcf2.variant == 'ORCF2'
1511 assert orcf3.variant == 'ORCF3'
1512 yield orcf1
1513 yield orcf2
1514 yield orcf3
1516 yield ORCI(a, b, c)
1517 yield ORCC(a, b, c)
1519 yield HEX(a, c)
1521 rhl1 = RHL(a, alpha=55.0)
1522 assert rhl1.variant == 'RHL1'
1523 yield rhl1
1525 rhl2 = RHL(a, alpha=105.0)
1526 assert rhl2.variant == 'RHL2'
1527 yield rhl2
1529 # With these lengths, alpha < 65 (or so) would result in a lattice that
1530 # could also be represented with alpha > 65, which is more conventional.
1531 yield MCL(a, b, c, alpha=70.0)
1533 mclc1 = MCLC(a, b, c, 80)
1534 assert mclc1.variant == 'MCLC1'
1535 yield mclc1
1536 # mclc2 has same special points as mclc1
1538 mclc3 = MCLC(1.8 * a, b, c * 2, 80)
1539 assert mclc3.variant == 'MCLC3'
1540 yield mclc3
1541 # mclc4 has same special points as mclc3
1543 # XXX We should add MCLC2 and MCLC4 as well.
1545 mclc5 = MCLC(b, b, 1.1 * b, 70)
1546 assert mclc5.variant == 'MCLC5'
1547 yield mclc5
1549 def get_tri(kcellpar):
1550 # We build the TRI lattices from cellpars of reciprocal cell
1551 icell = Cell.fromcellpar(kcellpar)
1552 cellpar = Cell(4 * icell.reciprocal()).cellpar()
1553 return TRI(*cellpar)
1555 tri1a = get_tri([1., 1.2, 1.4, 120., 110., 100.])
1556 assert tri1a.variant == 'TRI1a'
1557 yield tri1a
1559 tri1b = get_tri([1., 1.2, 1.4, 50., 60., 70.])
1560 assert tri1b.variant == 'TRI1b'
1561 yield tri1b
1563 tri2a = get_tri([1., 1.2, 1.4, 120., 110., 90.])
1564 assert tri2a.variant == 'TRI2a'
1565 yield tri2a
1566 tri2b = get_tri([1., 1.2, 1.4, 50., 60., 90.])
1567 assert tri2b.variant == 'TRI2b'
1568 yield tri2b
1570 # Choose an OBL lattice that round-trip-converts to itself.
1571 # The default a/b/alpha parameters result in another representation
1572 # of the same lattice.
1573 yield OBL(a=3.0, b=3.35, alpha=77.85)
1574 yield RECT(a, b)
1575 yield CRECT(a, alpha=alpha)
1576 yield HEX2D(a)
1577 yield SQR(a)
1578 yield LINE(a)