• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

materialsproject / pymatgen / 4075885785

pending completion
4075885785

push

github

Shyue Ping Ong
Merge branch 'master' of github.com:materialsproject/pymatgen

96 of 96 new or added lines in 27 files covered. (100.0%)

81013 of 102710 relevant lines covered (78.88%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.01
/pymatgen/analysis/gb/grain.py
1
# Copyright (c) Pymatgen Development Team.
2
# Distributed under the terms of the MIT License.
3

4
"""
1✔
5
Module containing classes to generate grain boundaries.
6
"""
7

8
from __future__ import annotations
1✔
9

10
import itertools
1✔
11
import logging
1✔
12
import warnings
1✔
13
from fractions import Fraction
1✔
14
from functools import reduce
1✔
15
from math import cos, floor, gcd
1✔
16
from typing import Sequence
1✔
17

18
import numpy as np
1✔
19
from monty.fractions import lcm
1✔
20

21
from pymatgen.core.lattice import Lattice
1✔
22
from pymatgen.core.sites import PeriodicSite
1✔
23
from pymatgen.core.structure import Structure
1✔
24
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
1✔
25

26
# This module implements representations of grain boundaries, as well as
27
# algorithms for generating them.
28

29
__author__ = "Xiang-Guo Li"
1✔
30
__copyright__ = "Copyright 2018, The Materials Virtual Lab"
1✔
31
__version__ = "0.1"
1✔
32
__maintainer__ = "Xiang-Guo Li"
1✔
33
__email__ = "xil110@ucsd.edu"
1✔
34
__date__ = "7/30/18"
1✔
35

36
logger = logging.getLogger(__name__)
1✔
37

38

39
class GrainBoundary(Structure):
1✔
40
    """
41
    Subclass of Structure representing a GrainBoundary (gb) object.
42
    Implements additional attributes pertaining to gbs, but the
43
    init method does not actually implement any algorithm that
44
    creates a gb. This is a DUMMY class who's init method only holds
45
    information about the gb. Also has additional methods that returns
46
    other information about a gb such as sigma value.
47

48
    Note that all gbs have the gb surface normal oriented in the c-direction.
49
    This means the lattice vectors a and b are in the gb surface plane (at
50
     least for one grain) and the c vector is out of the surface plane
51
     (though not necessary perpendicular to the surface.)
52
    """
53

54
    def __init__(
1✔
55
        self,
56
        lattice,
57
        species,
58
        coords,
59
        rotation_axis,
60
        rotation_angle,
61
        gb_plane,
62
        join_plane,
63
        init_cell,
64
        vacuum_thickness,
65
        ab_shift,
66
        site_properties,
67
        oriented_unit_cell,
68
        validate_proximity=False,
69
        coords_are_cartesian=False,
70
    ):
71
        """
72
        Makes a gb structure, a structure object with additional information
73
        and methods pertaining to gbs.
74

75
        Args:
76
            lattice (Lattice/3x3 array): The lattice, either as a
77
                :class:`pymatgen.core.lattice.Lattice` or
78
                simply as any 2D array. Each row should correspond to a lattice
79
                vector. E.g., [[10,0,0], [20,10,0], [0,0,30]] specifies a
80
                lattice with lattice vectors [10,0,0], [20,10,0] and [0,0,30].
81
            species ([Species]): Sequence of species on each site. Can take in
82
                flexible input, including:
83

84
                i.  A sequence of element / species specified either as string
85
                    symbols, e.g. ["Li", "Fe2+", "P", ...] or atomic numbers,
86
                    e.g., (3, 56, ...) or actual Element or Species objects.
87

88
                ii. List of dict of elements/species and occupancies, e.g.,
89
                    [{"Fe" : 0.5, "Mn":0.5}, ...]. This allows the setup of
90
                    disordered structures.
91
            coords (Nx3 array): list of fractional/cartesian coordinates of
92
                each species.
93
            rotation_axis (list): Rotation axis of GB in the form of a list of
94
                integers, e.g. [1, 1, 0].
95
            rotation_angle (float, in unit of degree): rotation angle of GB.
96
            gb_plane (list): Grain boundary plane in the form of a list of integers
97
                e.g.: [1, 2, 3].
98
            join_plane (list): Joining plane of the second grain in the form of a list of
99
                integers. e.g.: [1, 2, 3].
100
            init_cell (Structure): initial bulk structure to form the GB.
101
            site_properties (dict): Properties associated with the sites as a
102
                dict of sequences, The sequences have to be the same length as
103
                the atomic species and fractional_coords. For gb, you should
104
                have the 'grain_label' properties to classify the sites as 'top',
105
                'bottom', 'top_incident', or 'bottom_incident'.
106
            vacuum_thickness (float in angstrom): The thickness of vacuum inserted
107
                between two grains of the GB.
108
            ab_shift (list of float, in unit of crystal vector a, b): The relative
109
                shift along a, b vectors.
110
            oriented_unit_cell (Structure): oriented unit cell of the bulk init_cell.
111
                Help to accurate calculate the bulk properties that are consistent
112
                with gb calculations.
113
            validate_proximity (bool): Whether to check if there are sites
114
                that are less than 0.01 Ang apart. Defaults to False.
115
            coords_are_cartesian (bool): Set to True if you are providing
116
                coordinates in Cartesian coordinates. Defaults to False.
117
        """
118
        self.oriented_unit_cell = oriented_unit_cell
1✔
119
        self.rotation_axis = rotation_axis
1✔
120
        self.rotation_angle = rotation_angle
1✔
121
        self.gb_plane = gb_plane
1✔
122
        self.join_plane = join_plane
1✔
123
        self.init_cell = init_cell
1✔
124
        self.vacuum_thickness = vacuum_thickness
1✔
125
        self.ab_shift = ab_shift
1✔
126
        super().__init__(
1✔
127
            lattice,
128
            species,
129
            coords,
130
            validate_proximity=validate_proximity,
131
            coords_are_cartesian=coords_are_cartesian,
132
            site_properties=site_properties,
133
        )
134

135
    def copy(self):
1✔
136
        """
137
        Convenience method to get a copy of the structure, with options to add
138
        site properties.
139

140
        Returns:
141
            A copy of the Structure, with optionally new site_properties and
142
            optionally sanitized.
143
        """
144
        return GrainBoundary(
1✔
145
            self.lattice,
146
            self.species_and_occu,
147
            self.frac_coords,
148
            self.rotation_axis,
149
            self.rotation_angle,
150
            self.gb_plane,
151
            self.join_plane,
152
            self.init_cell,
153
            self.vacuum_thickness,
154
            self.ab_shift,
155
            self.site_properties,
156
            self.oriented_unit_cell,
157
        )
158

159
    def get_sorted_structure(self, key=None, reverse=False):
1✔
160
        """
161
        Get a sorted copy of the structure. The parameters have the same
162
        meaning as in list.sort. By default, sites are sorted by the
163
        electronegativity of the species. Note that Slab has to override this
164
        because of the different __init__ args.
165
        Args:
166
            key: Specifies a function of one argument that is used to extract
167
                a comparison key from each list element: key=str.lower. The
168
                default value is None (compare the elements directly).
169
            reverse (bool): If set to True, then the list elements are sorted
170
                as if each comparison were reversed.
171
        """
172
        sites = sorted(self, key=key, reverse=reverse)
×
173
        s = Structure.from_sites(sites)
×
174
        return GrainBoundary(
×
175
            s.lattice,
176
            s.species_and_occu,
177
            s.frac_coords,
178
            self.rotation_axis,
179
            self.rotation_angle,
180
            self.gb_plane,
181
            self.join_plane,
182
            self.init_cell,
183
            self.vacuum_thickness,
184
            self.ab_shift,
185
            self.site_properties,
186
            self.oriented_unit_cell,
187
        )
188

189
    @property
1✔
190
    def sigma(self):
1✔
191
        """
192
        This method returns the sigma value of the gb.
193
        If using 'quick_gen' to generate GB, this value is not valid.
194
        """
195
        return int(round(self.oriented_unit_cell.volume / self.init_cell.volume))
1✔
196

197
    @property
1✔
198
    def sigma_from_site_prop(self):
1✔
199
        """
200
        This method returns the sigma value of the gb from site properties.
201
        If the GB structure merge some atoms due to the atoms too closer with
202
        each other, this property will not work.
203
        """
204
        num_coi = 0
×
205
        if None in self.site_properties["grain_label"]:
×
206
            raise RuntimeError("Site were merged, this property do not work")
×
207
        for tag in self.site_properties["grain_label"]:
×
208
            if "incident" in tag:
×
209
                num_coi += 1
×
210
        return int(round(self.num_sites / num_coi))
×
211

212
    @property
1✔
213
    def top_grain(self):
1✔
214
        """
215
        return the top grain (Structure) of the GB.
216
        """
217
        top_sites = []
1✔
218
        for i, tag in enumerate(self.site_properties["grain_label"]):
1✔
219
            if "top" in tag:
1✔
220
                top_sites.append(self.sites[i])
1✔
221
        return Structure.from_sites(top_sites)
1✔
222

223
    @property
1✔
224
    def bottom_grain(self):
1✔
225
        """
226
        return the bottom grain (Structure) of the GB.
227
        """
228
        bottom_sites = []
1✔
229
        for i, tag in enumerate(self.site_properties["grain_label"]):
1✔
230
            if "bottom" in tag:
1✔
231
                bottom_sites.append(self.sites[i])
1✔
232
        return Structure.from_sites(bottom_sites)
1✔
233

234
    @property
1✔
235
    def coincidents(self):
1✔
236
        """
237
        return the a list of coincident sites.
238
        """
239
        coincident_sites = []
1✔
240
        for i, tag in enumerate(self.site_properties["grain_label"]):
1✔
241
            if "incident" in tag:
1✔
242
                coincident_sites.append(self.sites[i])
1✔
243
        return coincident_sites
1✔
244

245
    def __str__(self):
1✔
246
        comp = self.composition
×
247
        outs = [
×
248
            f"Gb Summary ({comp.formula})",
249
            f"Reduced Formula: {comp.reduced_formula}",
250
            f"Rotation axis: {self.rotation_axis}",
251
            f"Rotation angle: {self.rotation_angle}",
252
            f"GB plane: {self.gb_plane}",
253
            f"Join plane: {self.join_plane}",
254
            f"vacuum thickness: {self.vacuum_thickness}",
255
            f"ab_shift: {self.ab_shift}",
256
        ]
257

258
        def to_s(x, rjust=10):
×
259
            return (f"{x:0.6f}").rjust(rjust)
×
260

261
        outs.append("abc   : " + " ".join(to_s(i) for i in self.lattice.abc))
×
262
        outs.append("angles: " + " ".join(to_s(i) for i in self.lattice.angles))
×
263
        outs.append(f"Sites ({len(self)})")
×
264
        for i, site in enumerate(self):
×
265
            outs.append(
×
266
                " ".join(
267
                    [
268
                        str(i + 1),
269
                        site.species_string,
270
                        " ".join(to_s(j, 12) for j in site.frac_coords),
271
                    ]
272
                )
273
            )
274
        return "\n".join(outs)
×
275

276
    def as_dict(self):
1✔
277
        """
278
        Returns:
279
            Dictionary representation of GrainBoundary object
280
        """
281
        d = super().as_dict()
1✔
282
        d["@module"] = type(self).__module__
1✔
283
        d["@class"] = type(self).__name__
1✔
284
        d["init_cell"] = self.init_cell.as_dict()
1✔
285
        d["rotation_axis"] = self.rotation_axis
1✔
286
        d["rotation_angle"] = self.rotation_angle
1✔
287
        d["gb_plane"] = self.gb_plane
1✔
288
        d["join_plane"] = self.join_plane
1✔
289
        d["vacuum_thickness"] = self.vacuum_thickness
1✔
290
        d["ab_shift"] = self.ab_shift
1✔
291
        d["oriented_unit_cell"] = self.oriented_unit_cell.as_dict()
1✔
292
        return d
1✔
293

294
    @classmethod
1✔
295
    def from_dict(cls, d):
1✔
296
        """
297
        Generates a GrainBoundary object from a dictionary created by as_dict().
298

299
        Args:
300
            d: dict
301

302
        Returns:
303
            GrainBoundary object
304
        """
305
        lattice = Lattice.from_dict(d["lattice"])
1✔
306
        sites = [PeriodicSite.from_dict(sd, lattice) for sd in d["sites"]]
1✔
307
        s = Structure.from_sites(sites)
1✔
308

309
        return GrainBoundary(
1✔
310
            lattice=lattice,
311
            species=s.species_and_occu,
312
            coords=s.frac_coords,
313
            rotation_axis=d["rotation_axis"],
314
            rotation_angle=d["rotation_angle"],
315
            gb_plane=d["gb_plane"],
316
            join_plane=d["join_plane"],
317
            init_cell=Structure.from_dict(d["init_cell"]),
318
            vacuum_thickness=d["vacuum_thickness"],
319
            ab_shift=d["ab_shift"],
320
            oriented_unit_cell=Structure.from_dict(d["oriented_unit_cell"]),
321
            site_properties=s.site_properties,
322
        )
323

324

325
class GrainBoundaryGenerator:
1✔
326
    """
327
    This class is to generate grain boundaries (GBs) from bulk
328
    conventional cell (fcc, bcc can from the primitive cell), and works for Cubic,
329
    Tetragonal, Orthorhombic, Rhombohedral, and Hexagonal systems.
330
    It generate GBs from given parameters, which includes
331
    GB plane, rotation axis, rotation angle.
332

333
    This class works for any general GB, including twist, tilt and mixed GBs.
334
    The three parameters, rotation axis, GB plane and rotation angle, are
335
    sufficient to identify one unique GB. While sometimes, users may not be able
336
    to tell what exactly rotation angle is but prefer to use sigma as an parameter,
337
    this class also provides the function that is able to return all possible
338
    rotation angles for a specific sigma value.
339
    The same sigma value (with rotation axis fixed) can correspond to
340
    multiple rotation angles.
341
    Users can use structure matcher in pymatgen to get rid of the redundant structures.
342
    """
343

344
    def __init__(self, initial_structure, symprec: float = 0.1, angle_tolerance=1):
1✔
345
        """
346
        initial_structure (Structure): Initial input structure. It can
347
               be conventional or primitive cell (primitive cell works for bcc and fcc).
348
               For fcc and bcc, using conventional cell can lead to a non-primitive
349
               grain boundary structure.
350
               This code supplies Cubic, Tetragonal, Orthorhombic, Rhombohedral, and
351
               Hexagonal systems.
352
        symprec (float): Tolerance for symmetry finding. Defaults to 0.1 (the value used
353
                in Materials Project), which is for structures with slight deviations
354
                from their proper atomic positions (e.g., structures relaxed with
355
                electronic structure codes).
356
                A smaller value of 0.01 is often used for properly refined
357
                structures with atoms in the proper symmetry coordinates.
358
                User should make sure the symmetry is what you want.
359
        angle_tolerance (float): Angle tolerance for symmetry finding.
360
        """
361
        analyzer = SpacegroupAnalyzer(initial_structure, symprec, angle_tolerance)
1✔
362
        self.lat_type = analyzer.get_lattice_type()[0]
1✔
363
        if self.lat_type == "t":
1✔
364
            # need to use the conventional cell for tetragonal
365
            initial_structure = analyzer.get_conventional_standard_structure()
1✔
366
            a, b, c = initial_structure.lattice.abc
1✔
367
            # c axis of tetragonal structure not in the third direction
368
            if abs(a - b) > symprec:
1✔
369
                # a == c, rotate b to the third direction
370
                if abs(a - c) < symprec:
×
371
                    initial_structure.make_supercell([[0, 0, 1], [1, 0, 0], [0, 1, 0]])
×
372
                # b == c, rotate a to the third direction
373
                else:
374
                    initial_structure.make_supercell([[0, 1, 0], [0, 0, 1], [1, 0, 0]])
×
375
        elif self.lat_type == "h":
1✔
376
            alpha, beta, gamma = initial_structure.lattice.angles
1✔
377
            # c axis is not in the third direction
378
            if abs(gamma - 90) < angle_tolerance:
1✔
379
                # alpha = 120 or 60, rotate b, c to a, b vectors
380
                if abs(alpha - 90) > angle_tolerance:
×
381
                    initial_structure.make_supercell([[0, 1, 0], [0, 0, 1], [1, 0, 0]])
×
382
                # beta = 120 or 60, rotate c, a to a, b vectors
383
                elif abs(beta - 90) > angle_tolerance:
×
384
                    initial_structure.make_supercell([[0, 0, 1], [1, 0, 0], [0, 1, 0]])
×
385
        elif self.lat_type == "r":
1✔
386
            # need to use primitive cell for rhombohedra
387
            initial_structure = analyzer.get_primitive_standard_structure()
1✔
388
        elif self.lat_type == "o":
1✔
389
            # need to use the conventional cell for orthorombic
390
            initial_structure = analyzer.get_conventional_standard_structure()
1✔
391
        self.initial_structure = initial_structure
1✔
392

393
    def gb_from_parameters(
1✔
394
        self,
395
        rotation_axis,
396
        rotation_angle,
397
        expand_times=4,
398
        vacuum_thickness=0.0,
399
        ab_shift: Sequence[float] = (0, 0),
400
        normal=False,
401
        ratio=None,
402
        plane=None,
403
        max_search=20,
404
        tol_coi=1.0e-8,
405
        rm_ratio=0.7,
406
        quick_gen=False,
407
    ):
408
        """
409
        Args:
410
            rotation_axis (list): Rotation axis of GB in the form of a list of integer
411
                e.g.: [1, 1, 0]
412
            rotation_angle (float, in unit of degree): rotation angle used to generate GB.
413
                Make sure the angle is accurate enough. You can use the enum* functions
414
                in this class to extract the accurate angle.
415
                e.g.: The rotation angle of sigma 3 twist GB with the rotation axis
416
                [1, 1, 1] and GB plane (1, 1, 1) can be 60.000000000 degree.
417
                If you do not know the rotation angle, but know the sigma value, we have
418
                provide the function get_rotation_angle_from_sigma which is able to return
419
                all the rotation angles of sigma value you provided.
420
            expand_times (int): The multiple times used to expand one unit grain to larger grain.
421
                This is used to tune the grain length of GB to warrant that the two GBs in one
422
                cell do not interact with each other. Default set to 4.
423
            vacuum_thickness (float, in angstrom): The thickness of vacuum that you want to insert
424
                between two grains of the GB. Default to 0.
425
            ab_shift (list of float, in unit of a, b vectors of Gb): in plane shift of two grains
426
            normal (logic):
427
                determine if need to require the c axis of top grain (first transformation matrix)
428
                perpendicular to the surface or not.
429
                default to false.
430
            ratio (list of integers):
431
                lattice axial ratio.
432
                For cubic system, ratio is not needed.
433
                For tetragonal system, ratio = [mu, mv], list of two integers,
434
                that is, mu/mv = c2/a2. If it is irrational, set it to none.
435
                For orthorhombic system, ratio = [mu, lam, mv], list of three integers,
436
                that is, mu:lam:mv = c2:b2:a2. If irrational for one axis, set it to None.
437
                e.g. mu:lam:mv = c2,None,a2, means b2 is irrational.
438
                For rhombohedral system, ratio = [mu, mv], list of two integers,
439
                that is, mu/mv is the ratio of (1+2*cos(alpha))/cos(alpha).
440
                If irrational, set it to None.
441
                For hexagonal system, ratio = [mu, mv], list of two integers,
442
                that is, mu/mv = c2/a2. If it is irrational, set it to none.
443
                This code also supplies a class method to generate the ratio from the
444
                structure (get_ratio). User can also make their own approximation and
445
                input the ratio directly.
446
            plane (list): Grain boundary plane in the form of a list of integers
447
                e.g.: [1, 2, 3]. If none, we set it as twist GB. The plane will be perpendicular
448
                to the rotation axis.
449
            max_search (int): max search for the GB lattice vectors that give the smallest GB
450
                lattice. If normal is true, also max search the GB c vector that perpendicular
451
                to the plane. For complex GB, if you want to speed up, you can reduce this value.
452
                But too small of this value may lead to error.
453
            tol_coi (float): tolerance to find the coincidence sites. When making approximations to
454
                the ratio needed to generate the GB, you probably need to increase this tolerance to
455
                obtain the correct number of coincidence sites. To check the number of coincidence
456
                sites are correct or not, you can compare the generated Gb object's sigma_from_site_prop
457
                with enum* sigma values (what user expected by input).
458
            rm_ratio (float): the criteria to remove the atoms which are too close with each other.
459
                rm_ratio*bond_length of bulk system is the criteria of bond length, below which the atom
460
                will be removed. Default to 0.7.
461
            quick_gen (bool): whether to quickly generate a supercell, if set to true, no need to
462
                find the smallest cell.
463

464
        Returns:
465
           Grain boundary structure (gb object).
466
        """
467
        lat_type = self.lat_type
1✔
468
        # if the initial structure is primitive cell in cubic system,
469
        # calculate the transformation matrix from its conventional cell
470
        # to primitive cell, basically for bcc and fcc systems.
471
        trans_cry = np.eye(3)
1✔
472
        if lat_type == "c":
1✔
473
            analyzer = SpacegroupAnalyzer(self.initial_structure)
1✔
474
            convention_cell = analyzer.get_conventional_standard_structure()
1✔
475
            vol_ratio = self.initial_structure.volume / convention_cell.volume
1✔
476
            # bcc primitive cell, belong to cubic system
477
            if abs(vol_ratio - 0.5) < 1.0e-3:
1✔
478
                trans_cry = np.array([[0.5, 0.5, -0.5], [-0.5, 0.5, 0.5], [0.5, -0.5, 0.5]])
×
479
                logger.info("Make sure this is for cubic with bcc primitive cell")
×
480
            # fcc primitive cell, belong to cubic system
481
            elif abs(vol_ratio - 0.25) < 1.0e-3:
1✔
482
                trans_cry = np.array([[0.5, 0.5, 0], [0, 0.5, 0.5], [0.5, 0, 0.5]])
1✔
483
                logger.info("Make sure this is for cubic with fcc primitive cell")
1✔
484
            else:
485
                logger.info("Make sure this is for cubic with conventional cell")
1✔
486
        elif lat_type == "t":
1✔
487
            logger.info("Make sure this is for tetragonal system")
1✔
488
            if ratio is None:
1✔
489
                logger.info("Make sure this is for irrational c2/a2")
×
490
            elif len(ratio) != 2:
1✔
491
                raise RuntimeError("Tetragonal system needs correct c2/a2 ratio")
×
492
        elif lat_type == "o":
1✔
493
            logger.info("Make sure this is for orthorhombic system")
1✔
494
            if ratio is None:
1✔
495
                raise RuntimeError("CSL does not exist if all axial ratios are irrational for an orthorhombic system")
×
496
            if len(ratio) != 3:
1✔
497
                raise RuntimeError("Orthorhombic system needs correct c2:b2:a2 ratio")
×
498
        elif lat_type == "h":
1✔
499
            logger.info("Make sure this is for hexagonal system")
1✔
500
            if ratio is None:
1✔
501
                logger.info("Make sure this is for irrational c2/a2")
1✔
502
            elif len(ratio) != 2:
1✔
503
                raise RuntimeError("Hexagonal system needs correct c2/a2 ratio")
×
504
        elif lat_type == "r":
1✔
505
            logger.info("Make sure this is for rhombohedral system")
1✔
506
            if ratio is None:
1✔
507
                logger.info("Make sure this is for irrational (1+2*cos(alpha)/cos(alpha) ratio")
×
508
            elif len(ratio) != 2:
1✔
509
                raise RuntimeError("Rhombohedral system needs correct (1+2*cos(alpha)/cos(alpha) ratio")
×
510
        else:
511
            raise RuntimeError(
×
512
                "Lattice type not implemented. This code works for cubic, "
513
                "tetragonal, orthorhombic, rhombohedral, hexagonal systems"
514
            )
515

516
        # transform four index notation to three index notation for hexagonal and rhombohedral
517
        if len(rotation_axis) == 4:
1✔
518
            u1 = rotation_axis[0]
×
519
            v1 = rotation_axis[1]
×
520
            w1 = rotation_axis[3]
×
521
            if lat_type.lower() == "h":
×
522
                u = 2 * u1 + v1
×
523
                v = 2 * v1 + u1
×
524
                w = w1
×
525
                rotation_axis = [u, v, w]
×
526
            elif lat_type.lower() == "r":
×
527
                u = 2 * u1 + v1 + w1
×
528
                v = v1 + w1 - u1
×
529
                w = w1 - 2 * v1 - u1
×
530
                rotation_axis = [u, v, w]
×
531

532
        # make sure gcd(rotation_axis)==1
533
        if reduce(gcd, rotation_axis) != 1:
1✔
534
            rotation_axis = [int(round(x / reduce(gcd, rotation_axis))) for x in rotation_axis]
×
535
        # transform four index notation to three index notation for plane
536
        if plane is not None:
1✔
537
            if len(plane) == 4:
1✔
538
                u1 = plane[0]
×
539
                v1 = plane[1]
×
540
                w1 = plane[3]
×
541
                plane = [u1, v1, w1]
×
542
        # set the plane for grain boundary when plane is None.
543
        if plane is None:
1✔
544
            if lat_type.lower() == "c":
1✔
545
                plane = rotation_axis
1✔
546
            else:
547
                if lat_type.lower() == "h":
1✔
548
                    if ratio is None:
1✔
549
                        c2_a2_ratio = 1.0
1✔
550
                    else:
551
                        c2_a2_ratio = ratio[0] / ratio[1]
1✔
552
                    metric = np.array([[1, -0.5, 0], [-0.5, 1, 0], [0, 0, c2_a2_ratio]])
1✔
553
                elif lat_type.lower() == "r":
1✔
554
                    if ratio is None:
1✔
555
                        cos_alpha = 0.5
×
556
                    else:
557
                        cos_alpha = 1.0 / (ratio[0] / ratio[1] - 2)
1✔
558
                    metric = np.array(
1✔
559
                        [
560
                            [1, cos_alpha, cos_alpha],
561
                            [cos_alpha, 1, cos_alpha],
562
                            [cos_alpha, cos_alpha, 1],
563
                        ]
564
                    )
565
                elif lat_type.lower() == "t":
1✔
566
                    if ratio is None:
1✔
567
                        c2_a2_ratio = 1.0
×
568
                    else:
569
                        c2_a2_ratio = ratio[0] / ratio[1]
1✔
570
                    metric = np.array([[1, 0, 0], [0, 1, 0], [0, 0, c2_a2_ratio]])
1✔
571
                elif lat_type.lower() == "o":
1✔
572
                    for i in range(3):
1✔
573
                        if ratio[i] is None:
1✔
574
                            ratio[i] = 1
×
575
                    metric = np.array(
1✔
576
                        [
577
                            [1, 0, 0],
578
                            [0, ratio[1] / ratio[2], 0],
579
                            [0, 0, ratio[0] / ratio[2]],
580
                        ]
581
                    )
582
                else:
583
                    raise RuntimeError("Lattice type has not implemented.")
×
584

585
                plane = np.matmul(rotation_axis, metric)
1✔
586
                fractions = [Fraction(x).limit_denominator() for x in plane]
1✔
587
                least_mul = reduce(lcm, [f.denominator for f in fractions])
1✔
588
                plane = [int(round(x * least_mul)) for x in plane]
1✔
589

590
        if reduce(gcd, plane) != 1:
1✔
591
            index = reduce(gcd, plane)
1✔
592
            plane = [int(round(x / index)) for x in plane]
1✔
593

594
        t1, t2 = self.get_trans_mat(
1✔
595
            r_axis=rotation_axis,
596
            angle=rotation_angle,
597
            normal=normal,
598
            trans_cry=trans_cry,
599
            lat_type=lat_type,
600
            ratio=ratio,
601
            surface=plane,
602
            max_search=max_search,
603
            quick_gen=quick_gen,
604
        )
605

606
        # find the join_plane
607
        if lat_type.lower() != "c":
1✔
608
            if lat_type.lower() == "h":
1✔
609
                if ratio is None:
1✔
610
                    mu, mv = [1, 1]
1✔
611
                else:
612
                    mu, mv = ratio
1✔
613
                trans_cry1 = np.array([[1, 0, 0], [-0.5, np.sqrt(3.0) / 2.0, 0], [0, 0, np.sqrt(mu / mv)]])
1✔
614
            elif lat_type.lower() == "r":
1✔
615
                if ratio is None:
1✔
616
                    c2_a2_ratio = 1.0
×
617
                else:
618
                    mu, mv = ratio
1✔
619
                    c2_a2_ratio = 3 / (2 - 6 * mv / mu)
1✔
620
                trans_cry1 = np.array(
1✔
621
                    [
622
                        [0.5, np.sqrt(3.0) / 6.0, 1.0 / 3 * np.sqrt(c2_a2_ratio)],
623
                        [-0.5, np.sqrt(3.0) / 6.0, 1.0 / 3 * np.sqrt(c2_a2_ratio)],
624
                        [0, -1 * np.sqrt(3.0) / 3.0, 1.0 / 3 * np.sqrt(c2_a2_ratio)],
625
                    ]
626
                )
627
            else:
628
                if lat_type.lower() == "t":
1✔
629
                    if ratio is None:
1✔
630
                        mu, mv = [1, 1]
×
631
                    else:
632
                        mu, mv = ratio
1✔
633
                    lam = mv
1✔
634
                elif lat_type.lower() == "o":
1✔
635
                    new_ratio = [1 if v is None else v for v in ratio]
1✔
636
                    mu, lam, mv = new_ratio
1✔
637
                trans_cry1 = np.array([[1, 0, 0], [0, np.sqrt(lam / mv), 0], [0, 0, np.sqrt(mu / mv)]])
1✔
638
        else:
639
            trans_cry1 = trans_cry
1✔
640
        grain_matrix = np.dot(t2, trans_cry1)
1✔
641
        plane_init = np.cross(grain_matrix[0], grain_matrix[1])
1✔
642
        if lat_type.lower() != "c":
1✔
643
            plane_init = np.dot(plane_init, trans_cry1.T)
1✔
644
        join_plane = self.vec_to_surface(plane_init)
1✔
645

646
        parent_structure = self.initial_structure.copy()
1✔
647
        # calculate the bond_length in bulk system.
648
        if len(parent_structure) == 1:
1✔
649
            temp_str = parent_structure.copy()
1✔
650
            temp_str.make_supercell([1, 1, 2])
1✔
651
            distance = temp_str.distance_matrix
1✔
652
        else:
653
            distance = parent_structure.distance_matrix
1✔
654
        bond_length = np.min(distance[np.nonzero(distance)])
1✔
655

656
        # top grain
657
        top_grain = fix_pbc(parent_structure * t1)
1✔
658

659
        # obtain the smallest oriended cell
660
        if normal and not quick_gen:
1✔
661
            t_temp = self.get_trans_mat(
1✔
662
                r_axis=rotation_axis,
663
                angle=rotation_angle,
664
                normal=False,
665
                trans_cry=trans_cry,
666
                lat_type=lat_type,
667
                ratio=ratio,
668
                surface=plane,
669
                max_search=max_search,
670
            )
671
            oriended_unit_cell = fix_pbc(parent_structure * t_temp[0])
1✔
672
            t_matrix = oriended_unit_cell.lattice.matrix
1✔
673
            normal_v_plane = np.cross(t_matrix[0], t_matrix[1])
1✔
674
            unit_normal_v = normal_v_plane / np.linalg.norm(normal_v_plane)
1✔
675
            unit_ab_adjust = (t_matrix[2] - np.dot(unit_normal_v, t_matrix[2]) * unit_normal_v) / np.dot(
1✔
676
                unit_normal_v, t_matrix[2]
677
            )
678
        else:
679
            oriended_unit_cell = top_grain.copy()
1✔
680
            unit_ab_adjust = 0.0
1✔
681

682
        # bottom grain, using top grain's lattice matrix
683
        bottom_grain = fix_pbc(parent_structure * t2, top_grain.lattice.matrix)
1✔
684

685
        # label both grains with 'top','bottom','top_incident','bottom_incident'
686
        n_sites = top_grain.num_sites
1✔
687
        t_and_b = Structure(
1✔
688
            top_grain.lattice,
689
            top_grain.species + bottom_grain.species,
690
            list(top_grain.frac_coords) + list(bottom_grain.frac_coords),
691
        )
692
        t_and_b_dis = t_and_b.lattice.get_all_distances(
1✔
693
            t_and_b.frac_coords[0:n_sites], t_and_b.frac_coords[n_sites : n_sites * 2]
694
        )
695
        index_incident = np.nonzero(t_and_b_dis < np.min(t_and_b_dis) + tol_coi)
1✔
696

697
        top_labels = []
1✔
698
        for i in range(n_sites):
1✔
699
            if i in index_incident[0]:
1✔
700
                top_labels.append("top_incident")
1✔
701
            else:
702
                top_labels.append("top")
1✔
703
        bottom_labels = []
1✔
704
        for i in range(n_sites):
1✔
705
            if i in index_incident[1]:
1✔
706
                bottom_labels.append("bottom_incident")
1✔
707
            else:
708
                bottom_labels.append("bottom")
1✔
709
        top_grain = Structure(
1✔
710
            Lattice(top_grain.lattice.matrix),
711
            top_grain.species,
712
            top_grain.frac_coords,
713
            site_properties={"grain_label": top_labels},
714
        )
715
        bottom_grain = Structure(
1✔
716
            Lattice(bottom_grain.lattice.matrix),
717
            bottom_grain.species,
718
            bottom_grain.frac_coords,
719
            site_properties={"grain_label": bottom_labels},
720
        )
721

722
        # expand both grains
723
        top_grain.make_supercell([1, 1, expand_times])
1✔
724
        bottom_grain.make_supercell([1, 1, expand_times])
1✔
725
        top_grain = fix_pbc(top_grain)
1✔
726
        bottom_grain = fix_pbc(bottom_grain)
1✔
727

728
        # determine the top-grain location.
729
        edge_b = 1.0 - max(bottom_grain.frac_coords[:, 2])
1✔
730
        edge_t = 1.0 - max(top_grain.frac_coords[:, 2])
1✔
731
        c_adjust = (edge_t - edge_b) / 2.0
1✔
732

733
        # construct all species
734
        all_species = []
1✔
735
        all_species.extend([site.specie for site in bottom_grain])
1✔
736
        all_species.extend([site.specie for site in top_grain])
1✔
737

738
        half_lattice = top_grain.lattice
1✔
739
        # calculate translation vector, perpendicular to the plane
740
        normal_v_plane = np.cross(half_lattice.matrix[0], half_lattice.matrix[1])
1✔
741
        unit_normal_v = normal_v_plane / np.linalg.norm(normal_v_plane)
1✔
742
        translation_v = unit_normal_v * vacuum_thickness
1✔
743

744
        # construct the final lattice
745
        whole_matrix_no_vac = np.array(half_lattice.matrix)
1✔
746
        whole_matrix_no_vac[2] = half_lattice.matrix[2] * 2
1✔
747
        whole_matrix_with_vac = whole_matrix_no_vac.copy()
1✔
748
        whole_matrix_with_vac[2] = whole_matrix_no_vac[2] + translation_v * 2
1✔
749
        whole_lat = Lattice(whole_matrix_with_vac)
1✔
750

751
        # construct the coords, move top grain with translation_v
752
        all_coords = []
1✔
753
        grain_labels = bottom_grain.site_properties["grain_label"] + top_grain.site_properties["grain_label"]
1✔
754
        for site in bottom_grain:
1✔
755
            all_coords.append(site.coords)
1✔
756
        for site in top_grain:
1✔
757
            all_coords.append(
1✔
758
                site.coords
759
                + half_lattice.matrix[2] * (1 + c_adjust)
760
                + unit_ab_adjust * np.linalg.norm(half_lattice.matrix[2] * (1 + c_adjust))
761
                + translation_v
762
                + ab_shift[0] * whole_matrix_with_vac[0]
763
                + ab_shift[1] * whole_matrix_with_vac[1]
764
            )
765

766
        gb_with_vac = Structure(
1✔
767
            whole_lat,
768
            all_species,
769
            all_coords,
770
            coords_are_cartesian=True,
771
            site_properties={"grain_label": grain_labels},
772
        )
773
        # merge closer atoms. extract near gb atoms.
774
        cos_c_norm_plane = np.dot(unit_normal_v, whole_matrix_with_vac[2]) / whole_lat.c
1✔
775
        range_c_len = abs(bond_length / cos_c_norm_plane / whole_lat.c)
1✔
776
        sites_near_gb = []
1✔
777
        sites_away_gb: list[PeriodicSite] = []
1✔
778
        for site in gb_with_vac.sites:
1✔
779
            if (
1✔
780
                site.frac_coords[2] < range_c_len
781
                or site.frac_coords[2] > 1 - range_c_len
782
                or (site.frac_coords[2] > 0.5 - range_c_len and site.frac_coords[2] < 0.5 + range_c_len)
783
            ):
784
                sites_near_gb.append(site)
1✔
785
            else:
786
                sites_away_gb.append(site)
1✔
787
        if len(sites_near_gb) >= 1:
1✔
788
            s_near_gb = Structure.from_sites(sites_near_gb)
1✔
789
            s_near_gb.merge_sites(tol=bond_length * rm_ratio, mode="d")
1✔
790
            all_sites = sites_away_gb + s_near_gb.sites  # type: ignore
1✔
791
            gb_with_vac = Structure.from_sites(all_sites)
1✔
792

793
        # move coordinates into the periodic cell.
794
        gb_with_vac = fix_pbc(gb_with_vac, whole_lat.matrix)
1✔
795
        return GrainBoundary(
1✔
796
            whole_lat,
797
            gb_with_vac.species,
798
            gb_with_vac.cart_coords,
799
            rotation_axis,
800
            rotation_angle,
801
            plane,
802
            join_plane,
803
            self.initial_structure,
804
            vacuum_thickness,
805
            ab_shift,
806
            site_properties=gb_with_vac.site_properties,
807
            oriented_unit_cell=oriended_unit_cell,
808
            coords_are_cartesian=True,
809
        )
810

811
    def get_ratio(self, max_denominator=5, index_none=None):
1✔
812
        """
813
        find the axial ratio needed for GB generator input.
814
        Args:
815
            max_denominator (int): the maximum denominator for
816
                the computed ratio, default to be 5.
817
            index_none (int): specify the irrational axis.
818
                0-a, 1-b, 2-c. Only may be needed for orthorhombic system.
819
        Returns:
820
               axial ratio needed for GB generator (list of integers).
821
        """
822
        structure = self.initial_structure
1✔
823
        lat_type = self.lat_type
1✔
824
        if lat_type in ("t", "h"):
1✔
825
            # For tetragonal and hexagonal system, ratio = c2 / a2.
826
            a, _, c = structure.lattice.lengths
1✔
827
            if c > a:
1✔
828
                frac = Fraction(c**2 / a**2).limit_denominator(max_denominator)
1✔
829
                ratio = [frac.numerator, frac.denominator]
1✔
830
            else:
831
                frac = Fraction(a**2 / c**2).limit_denominator(max_denominator)
1✔
832
                ratio = [frac.denominator, frac.numerator]
1✔
833
        elif lat_type == "r":
1✔
834
            # For rhombohedral system, ratio = (1 + 2 * cos(alpha)) / cos(alpha).
835
            cos_alpha = cos(structure.lattice.alpha / 180 * np.pi)
1✔
836
            frac = Fraction((1 + 2 * cos_alpha) / cos_alpha).limit_denominator(max_denominator)
1✔
837
            ratio = [frac.numerator, frac.denominator]
1✔
838
        elif lat_type == "o":
1✔
839
            # For orthorhombic system, ratio = c2:b2:a2.If irrational for one axis, set it to None.
840
            ratio = [None] * 3
1✔
841
            lat = (structure.lattice.c, structure.lattice.b, structure.lattice.a)
1✔
842
            index = [0, 1, 2]
1✔
843
            if index_none is None:
1✔
844
                min_index = np.argmin(lat)
1✔
845
                index.pop(min_index)
1✔
846
                frac1 = Fraction(lat[index[0]] ** 2 / lat[min_index] ** 2).limit_denominator(max_denominator)
1✔
847
                frac2 = Fraction(lat[index[1]] ** 2 / lat[min_index] ** 2).limit_denominator(max_denominator)
1✔
848
                com_lcm = lcm(frac1.denominator, frac2.denominator)
1✔
849
                ratio[min_index] = com_lcm
1✔
850
                ratio[index[0]] = frac1.numerator * int(round(com_lcm / frac1.denominator))
1✔
851
                ratio[index[1]] = frac2.numerator * int(round(com_lcm / frac2.denominator))
1✔
852
            else:
853
                index.pop(index_none)
×
854
                if lat[index[0]] > lat[index[1]]:
×
855
                    frac = Fraction(lat[index[0]] ** 2 / lat[index[1]] ** 2).limit_denominator(max_denominator)
×
856
                    ratio[index[0]] = frac.numerator
×
857
                    ratio[index[1]] = frac.denominator
×
858
                else:
859
                    frac = Fraction(lat[index[1]] ** 2 / lat[index[0]] ** 2).limit_denominator(max_denominator)
×
860
                    ratio[index[1]] = frac.numerator
×
861
                    ratio[index[0]] = frac.denominator
×
862
        elif lat_type == "c":
1✔
863
            # Cubic system does not need axial ratio.
864
            return None
1✔
865
        else:
866
            raise RuntimeError("Lattice type not implemented.")
×
867
        return ratio
1✔
868

869
    @staticmethod
1✔
870
    def get_trans_mat(
1✔
871
        r_axis,
872
        angle,
873
        normal=False,
874
        trans_cry=None,
875
        lat_type="c",
876
        ratio=None,
877
        surface=None,
878
        max_search=20,
879
        quick_gen=False,
880
    ):
881
        """
882
        Find the two transformation matrix for each grain from given rotation axis,
883
        GB plane, rotation angle and corresponding ratio (see explanation for ratio
884
        below).
885
        The structure of each grain can be obtained by applying the corresponding
886
        transformation matrix to the conventional cell.
887
        The algorithm for this code is from reference, Acta Cryst, A32,783(1976).
888

889
        Args:
890
            r_axis (list of three integers, e.g. u, v, w
891
                    or four integers, e.g. u, v, t, w for hex/rho system only):
892
                    the rotation axis of the grain boundary.
893
            angle (float, in unit of degree) :
894
                    the rotation angle of the grain boundary
895
            normal (logic):
896
                    determine if need to require the c axis of one grain associated with
897
                    the first transformation matrix perpendicular to the surface or not.
898
                    default to false.
899
            trans_cry (3 by 3 array):
900
                    if the structure given are primitive cell in cubic system, e.g.
901
                    bcc or fcc system, trans_cry is the transformation matrix from its
902
                    conventional cell to the primitive cell.
903
            lat_type ( one character):
904
                    'c' or 'C': cubic system
905
                     't' or 'T': tetragonal system
906
                     'o' or 'O': orthorhombic system
907
                     'h' or 'H': hexagonal system
908
                     'r' or 'R': rhombohedral system
909
                     default to cubic system
910
            ratio (list of integers):
911
                    lattice axial ratio.
912
                    For cubic system, ratio is not needed.
913
                    For tetragonal system, ratio = [mu, mv], list of two integers,
914
                    that is, mu/mv = c2/a2. If it is irrational, set it to none.
915
                    For orthorhombic system, ratio = [mu, lam, mv], list of three integers,
916
                    that is, mu:lam:mv = c2:b2:a2. If irrational for one axis, set it to None.
917
                    e.g. mu:lam:mv = c2,None,a2, means b2 is irrational.
918
                    For rhombohedral system, ratio = [mu, mv], list of two integers,
919
                    that is, mu/mv is the ratio of (1+2*cos(alpha)/cos(alpha).
920
                    If irrational, set it to None.
921
                    For hexagonal system, ratio = [mu, mv], list of two integers,
922
                    that is, mu/mv = c2/a2. If it is irrational, set it to none.
923
            surface (list of three integers, e.g. h, k, l
924
                     or four integers, e.g. h, k, i, l for hex/rho system only):
925
                    the miller index of grain boundary plane, with the format of [h,k,l]
926
                    if surface is not given, the default is perpendicular to r_axis, which is
927
                    a twist grain boundary.
928
            max_search (int): max search for the GB lattice vectors that give the smallest GB
929
                lattice. If normal is true, also max search the GB c vector that perpendicular
930
                to the plane.
931
            quick_gen (bool): whether to quickly generate a supercell, if set to true, no need to
932
                find the smallest cell.
933
        Returns:
934
            t1 (3 by 3 integer array):
935
                    The transformation array for one grain.
936
            t2 (3 by 3 integer array):
937
                    The transformation array for the other grain
938
        """
939
        if trans_cry is None:
1✔
940
            trans_cry = np.eye(3)
1✔
941
        # transform four index notation to three index notation
942
        if len(r_axis) == 4:
1✔
943
            u1 = r_axis[0]
×
944
            v1 = r_axis[1]
×
945
            w1 = r_axis[3]
×
946
            if lat_type.lower() == "h":
×
947
                u = 2 * u1 + v1
×
948
                v = 2 * v1 + u1
×
949
                w = w1
×
950
                r_axis = [u, v, w]
×
951
            elif lat_type.lower() == "r":
×
952
                u = 2 * u1 + v1 + w1
×
953
                v = v1 + w1 - u1
×
954
                w = w1 - 2 * v1 - u1
×
955
                r_axis = [u, v, w]
×
956

957
        # make sure gcd(r_axis)==1
958
        if reduce(gcd, r_axis) != 1:
1✔
959
            r_axis = [int(round(x / reduce(gcd, r_axis))) for x in r_axis]
×
960

961
        if surface is not None:
1✔
962
            if len(surface) == 4:
1✔
963
                u1 = surface[0]
×
964
                v1 = surface[1]
×
965
                w1 = surface[3]
×
966
                surface = [u1, v1, w1]
×
967
        # set the surface for grain boundary.
968
        if surface is None:
1✔
969
            if lat_type.lower() == "c":
×
970
                surface = r_axis
×
971
            else:
972
                if lat_type.lower() == "h":
×
973
                    if ratio is None:
×
974
                        c2_a2_ratio = 1.0
×
975
                    else:
976
                        c2_a2_ratio = ratio[0] / ratio[1]
×
977
                    metric = np.array([[1, -0.5, 0], [-0.5, 1, 0], [0, 0, c2_a2_ratio]])
×
978
                elif lat_type.lower() == "r":
×
979
                    if ratio is None:
×
980
                        cos_alpha = 0.5
×
981
                    else:
982
                        cos_alpha = 1.0 / (ratio[0] / ratio[1] - 2)
×
983
                    metric = np.array(
×
984
                        [
985
                            [1, cos_alpha, cos_alpha],
986
                            [cos_alpha, 1, cos_alpha],
987
                            [cos_alpha, cos_alpha, 1],
988
                        ]
989
                    )
990
                elif lat_type.lower() == "t":
×
991
                    if ratio is None:
×
992
                        c2_a2_ratio = 1.0
×
993
                    else:
994
                        c2_a2_ratio = ratio[0] / ratio[1]
×
995
                    metric = np.array([[1, 0, 0], [0, 1, 0], [0, 0, c2_a2_ratio]])
×
996
                elif lat_type.lower() == "o":
×
997
                    for i in range(3):
×
998
                        if ratio[i] is None:
×
999
                            ratio[i] = 1
×
1000
                    metric = np.array(
×
1001
                        [
1002
                            [1, 0, 0],
1003
                            [0, ratio[1] / ratio[2], 0],
1004
                            [0, 0, ratio[0] / ratio[2]],
1005
                        ]
1006
                    )
1007
                else:
1008
                    raise RuntimeError("Lattice type has not implemented.")
×
1009

1010
                surface = np.matmul(r_axis, metric)
×
1011
                fractions = [Fraction(x).limit_denominator() for x in surface]
×
1012
                least_mul = reduce(lcm, [f.denominator for f in fractions])
×
1013
                surface = [int(round(x * least_mul)) for x in surface]
×
1014

1015
        if reduce(gcd, surface) != 1:
1✔
1016
            index = reduce(gcd, surface)
×
1017
            surface = [int(round(x / index)) for x in surface]
×
1018

1019
        if lat_type.lower() == "h":
1✔
1020
            # set the value for u,v,w,mu,mv,m,n,d,x
1021
            # check the reference for the meaning of these parameters
1022
            u, v, w = r_axis
1✔
1023
            # make sure mu, mv are coprime integers.
1024
            if ratio is None:
1✔
1025
                mu, mv = [1, 1]
1✔
1026
                if w != 0:
1✔
1027
                    if u != 0 or (v != 0):
1✔
1028
                        raise RuntimeError("For irrational c2/a2, CSL only exist for [0,0,1] or [u,v,0] and m = 0")
×
1029
            else:
1030
                mu, mv = ratio
1✔
1031
            if gcd(mu, mv) != 1:
1✔
1032
                temp = gcd(mu, mv)
×
1033
                mu = int(round(mu / temp))
×
1034
                mv = int(round(mv / temp))
×
1035
            d = (u**2 + v**2 - u * v) * mv + w**2 * mu
1✔
1036
            if abs(angle - 180.0) < 1.0e0:
1✔
1037
                m = 0
1✔
1038
                n = 1
1✔
1039
            else:
1040
                fraction = Fraction(
1✔
1041
                    np.tan(angle / 2 / 180.0 * np.pi) / np.sqrt(float(d) / 3.0 / mu)
1042
                ).limit_denominator()
1043
                m = fraction.denominator
1✔
1044
                n = fraction.numerator
1✔
1045

1046
            # construct the rotation matrix, check reference for details
1047
            r_list = [
1✔
1048
                (u**2 * mv - v**2 * mv - w**2 * mu) * n**2 + 2 * w * mu * m * n + 3 * mu * m**2,
1049
                (2 * v - u) * u * mv * n**2 - 4 * w * mu * m * n,
1050
                2 * u * w * mu * n**2 + 2 * (2 * v - u) * mu * m * n,
1051
                (2 * u - v) * v * mv * n**2 + 4 * w * mu * m * n,
1052
                (v**2 * mv - u**2 * mv - w**2 * mu) * n**2 - 2 * w * mu * m * n + 3 * mu * m**2,
1053
                2 * v * w * mu * n**2 - 2 * (2 * u - v) * mu * m * n,
1054
                (2 * u - v) * w * mv * n**2 - 3 * v * mv * m * n,
1055
                (2 * v - u) * w * mv * n**2 + 3 * u * mv * m * n,
1056
                (w**2 * mu - u**2 * mv - v**2 * mv + u * v * mv) * n**2 + 3 * mu * m**2,
1057
            ]
1058
            m = -1 * m
1✔
1059
            r_list_inv = [
1✔
1060
                (u**2 * mv - v**2 * mv - w**2 * mu) * n**2 + 2 * w * mu * m * n + 3 * mu * m**2,
1061
                (2 * v - u) * u * mv * n**2 - 4 * w * mu * m * n,
1062
                2 * u * w * mu * n**2 + 2 * (2 * v - u) * mu * m * n,
1063
                (2 * u - v) * v * mv * n**2 + 4 * w * mu * m * n,
1064
                (v**2 * mv - u**2 * mv - w**2 * mu) * n**2 - 2 * w * mu * m * n + 3 * mu * m**2,
1065
                2 * v * w * mu * n**2 - 2 * (2 * u - v) * mu * m * n,
1066
                (2 * u - v) * w * mv * n**2 - 3 * v * mv * m * n,
1067
                (2 * v - u) * w * mv * n**2 + 3 * u * mv * m * n,
1068
                (w**2 * mu - u**2 * mv - v**2 * mv + u * v * mv) * n**2 + 3 * mu * m**2,
1069
            ]
1070
            m = -1 * m
1✔
1071
            F = 3 * mu * m**2 + d * n**2
1✔
1072
            all_list = r_list + r_list_inv + [F]
1✔
1073
            com_fac = reduce(gcd, all_list)
1✔
1074
            sigma = F / com_fac
1✔
1075
            r_matrix = (np.array(r_list) / com_fac / sigma).reshape(3, 3)
1✔
1076
        elif lat_type.lower() == "r":
1✔
1077
            # set the value for u,v,w,mu,mv,m,n,d
1078
            # check the reference for the meaning of these parameters
1079
            u, v, w = r_axis
1✔
1080
            # make sure mu, mv are coprime integers.
1081
            if ratio is None:
1✔
1082
                mu, mv = [1, 1]
×
1083
                if u + v + w != 0:
×
1084
                    if u != v or u != w:
×
1085
                        raise RuntimeError(
×
1086
                            "For irrational ratio_alpha, CSL only exist for [1,1,1] or [u, v, -(u+v)] and m =0"
1087
                        )
1088
            else:
1089
                mu, mv = ratio
1✔
1090
            if gcd(mu, mv) != 1:
1✔
1091
                temp = gcd(mu, mv)
×
1092
                mu = int(round(mu / temp))
×
1093
                mv = int(round(mv / temp))
×
1094
            d = (u**2 + v**2 + w**2) * (mu - 2 * mv) + 2 * mv * (v * w + w * u + u * v)
1✔
1095
            if abs(angle - 180.0) < 1.0e0:
1✔
1096
                m = 0
×
1097
                n = 1
×
1098
            else:
1099
                fraction = Fraction(np.tan(angle / 2 / 180.0 * np.pi) / np.sqrt(float(d) / mu)).limit_denominator()
1✔
1100
                m = fraction.denominator
1✔
1101
                n = fraction.numerator
1✔
1102

1103
            # construct the rotation matrix, check reference for details
1104
            r_list = [
1✔
1105
                (mu - 2 * mv) * (u**2 - v**2 - w**2) * n**2
1106
                + 2 * mv * (v - w) * m * n
1107
                - 2 * mv * v * w * n**2
1108
                + mu * m**2,
1109
                2 * (mv * u * n * (w * n + u * n - m) - (mu - mv) * m * w * n + (mu - 2 * mv) * u * v * n**2),
1110
                2 * (mv * u * n * (v * n + u * n + m) + (mu - mv) * m * v * n + (mu - 2 * mv) * w * u * n**2),
1111
                2 * (mv * v * n * (w * n + v * n + m) + (mu - mv) * m * w * n + (mu - 2 * mv) * u * v * n**2),
1112
                (mu - 2 * mv) * (v**2 - w**2 - u**2) * n**2
1113
                + 2 * mv * (w - u) * m * n
1114
                - 2 * mv * u * w * n**2
1115
                + mu * m**2,
1116
                2 * (mv * v * n * (v * n + u * n - m) - (mu - mv) * m * u * n + (mu - 2 * mv) * w * v * n**2),
1117
                2 * (mv * w * n * (w * n + v * n - m) - (mu - mv) * m * v * n + (mu - 2 * mv) * w * u * n**2),
1118
                2 * (mv * w * n * (w * n + u * n + m) + (mu - mv) * m * u * n + (mu - 2 * mv) * w * v * n**2),
1119
                (mu - 2 * mv) * (w**2 - u**2 - v**2) * n**2
1120
                + 2 * mv * (u - v) * m * n
1121
                - 2 * mv * u * v * n**2
1122
                + mu * m**2,
1123
            ]
1124
            m = -1 * m
1✔
1125
            r_list_inv = [
1✔
1126
                (mu - 2 * mv) * (u**2 - v**2 - w**2) * n**2
1127
                + 2 * mv * (v - w) * m * n
1128
                - 2 * mv * v * w * n**2
1129
                + mu * m**2,
1130
                2 * (mv * u * n * (w * n + u * n - m) - (mu - mv) * m * w * n + (mu - 2 * mv) * u * v * n**2),
1131
                2 * (mv * u * n * (v * n + u * n + m) + (mu - mv) * m * v * n + (mu - 2 * mv) * w * u * n**2),
1132
                2 * (mv * v * n * (w * n + v * n + m) + (mu - mv) * m * w * n + (mu - 2 * mv) * u * v * n**2),
1133
                (mu - 2 * mv) * (v**2 - w**2 - u**2) * n**2
1134
                + 2 * mv * (w - u) * m * n
1135
                - 2 * mv * u * w * n**2
1136
                + mu * m**2,
1137
                2 * (mv * v * n * (v * n + u * n - m) - (mu - mv) * m * u * n + (mu - 2 * mv) * w * v * n**2),
1138
                2 * (mv * w * n * (w * n + v * n - m) - (mu - mv) * m * v * n + (mu - 2 * mv) * w * u * n**2),
1139
                2 * (mv * w * n * (w * n + u * n + m) + (mu - mv) * m * u * n + (mu - 2 * mv) * w * v * n**2),
1140
                (mu - 2 * mv) * (w**2 - u**2 - v**2) * n**2
1141
                + 2 * mv * (u - v) * m * n
1142
                - 2 * mv * u * v * n**2
1143
                + mu * m**2,
1144
            ]
1145
            m = -1 * m
1✔
1146
            F = mu * m**2 + d * n**2
1✔
1147
            all_list = r_list_inv + r_list + [F]
1✔
1148
            com_fac = reduce(gcd, all_list)
1✔
1149
            sigma = F / com_fac
1✔
1150
            r_matrix = (np.array(r_list) / com_fac / sigma).reshape(3, 3)
1✔
1151
        else:
1152
            u, v, w = r_axis
1✔
1153
            if lat_type.lower() == "c":
1✔
1154
                mu = 1
1✔
1155
                lam = 1
1✔
1156
                mv = 1
1✔
1157
            elif lat_type.lower() == "t":
1✔
1158
                if ratio is None:
1✔
1159
                    mu, mv = [1, 1]
×
1160
                    if w != 0:
×
1161
                        if u != 0 or (v != 0):
×
1162
                            raise RuntimeError("For irrational c2/a2, CSL only exist for [0,0,1] or [u,v,0] and m = 0")
×
1163
                else:
1164
                    mu, mv = ratio
1✔
1165
                lam = mv
1✔
1166
            elif lat_type.lower() == "o":
1✔
1167
                if None in ratio:
1✔
1168
                    mu, lam, mv = ratio
×
1169
                    non_none = [i for i in ratio if i is not None]
×
1170
                    if len(non_none) < 2:
×
1171
                        raise RuntimeError("No CSL exist for two irrational numbers")
×
1172
                    non1, non2 = non_none
×
1173
                    if mu is None:
×
1174
                        lam = non1
×
1175
                        mv = non2
×
1176
                        mu = 1
×
1177
                        if w != 0:
×
1178
                            if u != 0 or (v != 0):
×
1179
                                raise RuntimeError("For irrational c2, CSL only exist for [0,0,1] or [u,v,0] and m = 0")
×
1180
                    elif lam is None:
×
1181
                        mu = non1
×
1182
                        mv = non2
×
1183
                        lam = 1
×
1184
                        if v != 0:
×
1185
                            if u != 0 or (w != 0):
×
1186
                                raise RuntimeError("For irrational b2, CSL only exist for [0,1,0] or [u,0,w] and m = 0")
×
1187
                    elif mv is None:
×
1188
                        mu = non1
×
1189
                        lam = non2
×
1190
                        mv = 1
×
1191
                        if u != 0:
×
1192
                            if w != 0 or (v != 0):
×
1193
                                raise RuntimeError("For irrational a2, CSL only exist for [1,0,0] or [0,v,w] and m = 0")
×
1194
                else:
1195
                    mu, lam, mv = ratio
1✔
1196
                    if u == 0 and v == 0:
1✔
1197
                        mu = 1
×
1198
                    if u == 0 and w == 0:
1✔
1199
                        lam = 1
×
1200
                    if v == 0 and w == 0:
1✔
1201
                        mv = 1
×
1202

1203
            # make sure mu, lambda, mv are coprime integers.
1204
            if reduce(gcd, [mu, lam, mv]) != 1:
1✔
1205
                temp = reduce(gcd, [mu, lam, mv])
×
1206
                mu = int(round(mu / temp))
×
1207
                mv = int(round(mv / temp))
×
1208
                lam = int(round(lam / temp))
×
1209
            d = (mv * u**2 + lam * v**2) * mv + w**2 * mu * mv
1✔
1210
            if abs(angle - 180.0) < 1.0e0:
1✔
1211
                m = 0
×
1212
                n = 1
×
1213
            else:
1214
                fraction = Fraction(np.tan(angle / 2 / 180.0 * np.pi) / np.sqrt(d / mu / lam)).limit_denominator()
1✔
1215
                m = fraction.denominator
1✔
1216
                n = fraction.numerator
1✔
1217
            r_list = [
1✔
1218
                (u**2 * mv * mv - lam * v**2 * mv - w**2 * mu * mv) * n**2 + lam * mu * m**2,
1219
                2 * lam * (v * u * mv * n**2 - w * mu * m * n),
1220
                2 * mu * (u * w * mv * n**2 + v * lam * m * n),
1221
                2 * mv * (u * v * mv * n**2 + w * mu * m * n),
1222
                (v**2 * mv * lam - u**2 * mv * mv - w**2 * mu * mv) * n**2 + lam * mu * m**2,
1223
                2 * mv * mu * (v * w * n**2 - u * m * n),
1224
                2 * mv * (u * w * mv * n**2 - v * lam * m * n),
1225
                2 * lam * mv * (v * w * n**2 + u * m * n),
1226
                (w**2 * mu * mv - u**2 * mv * mv - v**2 * mv * lam) * n**2 + lam * mu * m**2,
1227
            ]
1228
            m = -1 * m
1✔
1229
            r_list_inv = [
1✔
1230
                (u**2 * mv * mv - lam * v**2 * mv - w**2 * mu * mv) * n**2 + lam * mu * m**2,
1231
                2 * lam * (v * u * mv * n**2 - w * mu * m * n),
1232
                2 * mu * (u * w * mv * n**2 + v * lam * m * n),
1233
                2 * mv * (u * v * mv * n**2 + w * mu * m * n),
1234
                (v**2 * mv * lam - u**2 * mv * mv - w**2 * mu * mv) * n**2 + lam * mu * m**2,
1235
                2 * mv * mu * (v * w * n**2 - u * m * n),
1236
                2 * mv * (u * w * mv * n**2 - v * lam * m * n),
1237
                2 * lam * mv * (v * w * n**2 + u * m * n),
1238
                (w**2 * mu * mv - u**2 * mv * mv - v**2 * mv * lam) * n**2 + lam * mu * m**2,
1239
            ]
1240
            m = -1 * m
1✔
1241
            F = mu * lam * m**2 + d * n**2
1✔
1242
            all_list = r_list + r_list_inv + [F]
1✔
1243
            com_fac = reduce(gcd, all_list)
1✔
1244
            sigma = F / com_fac
1✔
1245
            r_matrix = (np.array(r_list) / com_fac / sigma).reshape(3, 3)
1✔
1246

1247
        if sigma > 1000:
1✔
1248
            raise RuntimeError("Sigma >1000 too large. Are you sure what you are doing, Please check the GB if exist")
×
1249
        # transform surface, r_axis, r_matrix in terms of primitive lattice
1250
        surface = np.matmul(surface, np.transpose(trans_cry))
1✔
1251
        fractions = [Fraction(x).limit_denominator() for x in surface]
1✔
1252
        least_mul = reduce(lcm, [f.denominator for f in fractions])
1✔
1253
        surface = [int(round(x * least_mul)) for x in surface]
1✔
1254
        if reduce(gcd, surface) != 1:
1✔
1255
            index = reduce(gcd, surface)
×
1256
            surface = [int(round(x / index)) for x in surface]
×
1257
        r_axis = np.rint(np.matmul(r_axis, np.linalg.inv(trans_cry))).astype(int)
1✔
1258
        if reduce(gcd, r_axis) != 1:
1✔
1259
            r_axis = [int(round(x / reduce(gcd, r_axis))) for x in r_axis]
1✔
1260
        r_matrix = np.dot(np.dot(np.linalg.inv(trans_cry.T), r_matrix), trans_cry.T)
1✔
1261
        # set one vector of the basis to the rotation axis direction, and
1262
        # obtain the corresponding transform matrix
1263
        eye = np.eye(3, dtype=int)
1✔
1264
        for h in range(3):
1✔
1265
            if abs(r_axis[h]) != 0:
1✔
1266
                eye[h] = np.array(r_axis)
1✔
1267
                k = h + 1 if h + 1 < 3 else abs(2 - h)
1✔
1268
                l = h + 2 if h + 2 < 3 else abs(1 - h)
1✔
1269
                break
1✔
1270
        trans = eye.T
1✔
1271
        new_rot = np.array(r_matrix)
1✔
1272

1273
        # with the rotation matrix to construct the CSL lattice, check reference for details
1274
        fractions = [Fraction(x).limit_denominator() for x in new_rot[:, k]]
1✔
1275
        least_mul = reduce(lcm, [f.denominator for f in fractions])
1✔
1276
        scale = np.zeros((3, 3))
1✔
1277
        scale[h, h] = 1
1✔
1278
        scale[k, k] = least_mul
1✔
1279
        scale[l, l] = sigma / least_mul
1✔
1280
        for i in range(least_mul):
1✔
1281
            check_int = i * new_rot[:, k] + (sigma / least_mul) * new_rot[:, l]
1✔
1282
            if all(np.round(x, 5).is_integer() for x in list(check_int)):
1✔
1283
                n_final = i
1✔
1284
                break
1✔
1285
        try:
1✔
1286
            n_final
1✔
1287
        except NameError:
×
1288
            raise RuntimeError("Something is wrong. Check if this GB exists or not")
×
1289
        scale[k, l] = n_final
1✔
1290
        # each row of mat_csl is the CSL lattice vector
1291
        csl_init = np.rint(np.dot(np.dot(r_matrix, trans), scale)).astype(int).T
1✔
1292
        if abs(r_axis[h]) > 1:
1✔
1293
            csl_init = GrainBoundaryGenerator.reduce_mat(np.array(csl_init), r_axis[h], r_matrix)
1✔
1294
        csl = np.rint(Lattice(csl_init).get_niggli_reduced_lattice().matrix).astype(int)
1✔
1295

1296
        # find the best slab supercell in terms of the conventional cell from the csl lattice,
1297
        # which is the transformation matrix
1298

1299
        # now trans_cry is the transformation matrix from crystal to Cartesian coordinates.
1300
        # for cubic, do not need to change.
1301
        if lat_type.lower() != "c":
1✔
1302
            if lat_type.lower() == "h":
1✔
1303
                trans_cry = np.array([[1, 0, 0], [-0.5, np.sqrt(3.0) / 2.0, 0], [0, 0, np.sqrt(mu / mv)]])
1✔
1304
            elif lat_type.lower() == "r":
1✔
1305
                if ratio is None:
1✔
1306
                    c2_a2_ratio = 1.0
×
1307
                else:
1308
                    c2_a2_ratio = 3.0 / (2 - 6 * mv / mu)
1✔
1309
                trans_cry = np.array(
1✔
1310
                    [
1311
                        [0.5, np.sqrt(3.0) / 6.0, 1.0 / 3 * np.sqrt(c2_a2_ratio)],
1312
                        [-0.5, np.sqrt(3.0) / 6.0, 1.0 / 3 * np.sqrt(c2_a2_ratio)],
1313
                        [0, -1 * np.sqrt(3.0) / 3.0, 1.0 / 3 * np.sqrt(c2_a2_ratio)],
1314
                    ]
1315
                )
1316
            else:
1317
                trans_cry = np.array([[1, 0, 0], [0, np.sqrt(lam / mv), 0], [0, 0, np.sqrt(mu / mv)]])
1✔
1318
        t1_final = GrainBoundaryGenerator.slab_from_csl(
1✔
1319
            csl, surface, normal, trans_cry, max_search=max_search, quick_gen=quick_gen
1320
        )
1321
        t2_final = np.array(np.rint(np.dot(t1_final, np.linalg.inv(r_matrix.T)))).astype(int)
1✔
1322
        return t1_final, t2_final
1✔
1323

1324
    @staticmethod
1✔
1325
    def enum_sigma_cubic(cutoff, r_axis):
1✔
1326
        """
1327
        Find all possible sigma values and corresponding rotation angles
1328
        within a sigma value cutoff with known rotation axis in cubic system.
1329
        The algorithm for this code is from reference, Acta Cryst, A40,108(1984)
1330
        Args:
1331
            cutoff (int): the cutoff of sigma values.
1332
            r_axis (list of three integers, e.g. u, v, w):
1333
                    the rotation axis of the grain boundary, with the format of [u,v,w].
1334
        Returns:
1335
            sigmas (dict):
1336
                    dictionary with keys as the possible integer sigma values
1337
                    and values as list of the possible rotation angles to the
1338
                    corresponding sigma values.
1339
                    e.g. the format as
1340
                    {sigma1: [angle11,angle12,...], sigma2: [angle21, angle22,...],...}
1341
                    Note: the angles are the rotation angles of one grain respect to
1342
                    the other grain.
1343
                    When generate the microstructures of the grain boundary using these angles,
1344
                    you need to analyze the symmetry of the structure. Different angles may
1345
                    result in equivalent microstructures.
1346
        """
1347
        sigmas = {}
1✔
1348
        # make sure gcd(r_axis)==1
1349
        if reduce(gcd, r_axis) != 1:
1✔
1350
            r_axis = [int(round(x / reduce(gcd, r_axis))) for x in r_axis]
1✔
1351

1352
        # count the number of odds in r_axis
1353
        odd_r = len(list(filter(lambda x: x % 2 == 1, r_axis)))
1✔
1354
        # Compute the max n we need to enumerate.
1355
        if odd_r == 3:
1✔
1356
            a_max = 4
1✔
1357
        elif odd_r == 0:
1✔
1358
            a_max = 1
×
1359
        else:
1360
            a_max = 2
1✔
1361
        n_max = int(np.sqrt(cutoff * a_max / sum(np.array(r_axis) ** 2)))
1✔
1362
        # enumerate all possible n, m to give possible sigmas within the cutoff.
1363
        for n_loop in range(1, n_max + 1):
1✔
1364
            n = n_loop
1✔
1365
            m_max = int(np.sqrt(cutoff * a_max - n**2 * sum(np.array(r_axis) ** 2)))
1✔
1366
            for m in range(0, m_max + 1):
1✔
1367
                if gcd(m, n) == 1 or m == 0:
1✔
1368
                    if m == 0:
1✔
1369
                        n = 1
1✔
1370
                    else:
1371
                        n = n_loop
1✔
1372
                    # construct the quadruple [m, U,V,W], count the number of odds in
1373
                    # quadruple to determine the parameter a, refer to the reference
1374
                    quadruple = [m] + [x * n for x in r_axis]
1✔
1375
                    odd_qua = len(list(filter(lambda x: x % 2 == 1, quadruple)))
1✔
1376
                    if odd_qua == 4:
1✔
1377
                        a = 4
1✔
1378
                    elif odd_qua == 2:
1✔
1379
                        a = 2
1✔
1380
                    else:
1381
                        a = 1
1✔
1382
                    sigma = int(round((m**2 + n**2 * sum(np.array(r_axis) ** 2)) / a))
1✔
1383
                    if 1 < sigma <= cutoff:
1✔
1384
                        if sigma not in list(sigmas):
1✔
1385
                            if m == 0:
1✔
1386
                                angle = 180.0
1✔
1387
                            else:
1388
                                angle = 2 * np.arctan(n * np.sqrt(sum(np.array(r_axis) ** 2)) / m) / np.pi * 180
1✔
1389
                            sigmas[sigma] = [angle]
1✔
1390
                        else:
1391
                            if m == 0:
1✔
1392
                                angle = 180.0
1✔
1393
                            else:
1394
                                angle = 2 * np.arctan(n * np.sqrt(sum(np.array(r_axis) ** 2)) / m) / np.pi * 180
1✔
1395
                            if angle not in sigmas[sigma]:
1✔
1396
                                sigmas[sigma].append(angle)
1✔
1397
        return sigmas
1✔
1398

1399
    @staticmethod
1✔
1400
    def enum_sigma_hex(cutoff, r_axis, c2_a2_ratio):
1✔
1401
        """
1402
        Find all possible sigma values and corresponding rotation angles
1403
        within a sigma value cutoff with known rotation axis in hexagonal system.
1404
        The algorithm for this code is from reference, Acta Cryst, A38,550(1982)
1405

1406
        Args:
1407
            cutoff (int): the cutoff of sigma values.
1408
            r_axis (list of three integers, e.g. u, v, w
1409
                    or four integers, e.g. u, v, t, w):
1410
                    the rotation axis of the grain boundary.
1411
            c2_a2_ratio (list of two integers, e.g. mu, mv):
1412
                    mu/mv is the square of the hexagonal axial ratio, which is rational
1413
                    number. If irrational, set c2_a2_ratio = None
1414
        Returns:
1415
            sigmas (dict):
1416
                    dictionary with keys as the possible integer sigma values
1417
                    and values as list of the possible rotation angles to the
1418
                    corresponding sigma values.
1419
                    e.g. the format as
1420
                    {sigma1: [angle11,angle12,...], sigma2: [angle21, angle22,...],...}
1421
                    Note: the angles are the rotation angle of one grain respect to the
1422
                    other grain.
1423
                    When generate the microstructure of the grain boundary using these
1424
                    angles, you need to analyze the symmetry of the structure. Different
1425
                    angles may result in equivalent microstructures.
1426
        """
1427
        sigmas = {}
1✔
1428
        # make sure gcd(r_axis)==1
1429
        if reduce(gcd, r_axis) != 1:
1✔
1430
            r_axis = [int(round(x / reduce(gcd, r_axis))) for x in r_axis]
1✔
1431
        # transform four index notation to three index notation
1432
        if len(r_axis) == 4:
1✔
1433
            u1 = r_axis[0]
×
1434
            v1 = r_axis[1]
×
1435
            w1 = r_axis[3]
×
1436
            u = 2 * u1 + v1
×
1437
            v = 2 * v1 + u1
×
1438
            w = w1
×
1439
        else:
1440
            u, v, w = r_axis
1✔
1441

1442
        # make sure mu, mv are coprime integers.
1443
        if c2_a2_ratio is None:
1✔
1444
            mu, mv = [1, 1]
×
1445
            if w != 0:
×
1446
                if u != 0 or (v != 0):
×
1447
                    raise RuntimeError("For irrational c2/a2, CSL only exist for [0,0,1] or [u,v,0] and m = 0")
×
1448
        else:
1449
            mu, mv = c2_a2_ratio
1✔
1450
            if gcd(mu, mv) != 1:
1✔
1451
                temp = gcd(mu, mv)
×
1452
                mu = int(round(mu / temp))
×
1453
                mv = int(round(mv / temp))
×
1454

1455
        # refer to the meaning of d in reference
1456
        d = (u**2 + v**2 - u * v) * mv + w**2 * mu
1✔
1457

1458
        # Compute the max n we need to enumerate.
1459
        n_max = int(np.sqrt((cutoff * 12 * mu * mv) / abs(d)))
1✔
1460

1461
        # Enumerate all possible n, m to give possible sigmas within the cutoff.
1462
        for n in range(1, n_max + 1):
1✔
1463
            if (c2_a2_ratio is None) and w == 0:
1✔
1464
                m_max = 0
×
1465
            else:
1466
                m_max = int(np.sqrt((cutoff * 12 * mu * mv - n**2 * d) / (3 * mu)))
1✔
1467
            for m in range(0, m_max + 1):
1✔
1468
                if gcd(m, n) == 1 or m == 0:
1✔
1469
                    # construct the rotation matrix, refer to the reference
1470
                    R_list = [
1✔
1471
                        (u**2 * mv - v**2 * mv - w**2 * mu) * n**2 + 2 * w * mu * m * n + 3 * mu * m**2,
1472
                        (2 * v - u) * u * mv * n**2 - 4 * w * mu * m * n,
1473
                        2 * u * w * mu * n**2 + 2 * (2 * v - u) * mu * m * n,
1474
                        (2 * u - v) * v * mv * n**2 + 4 * w * mu * m * n,
1475
                        (v**2 * mv - u**2 * mv - w**2 * mu) * n**2 - 2 * w * mu * m * n + 3 * mu * m**2,
1476
                        2 * v * w * mu * n**2 - 2 * (2 * u - v) * mu * m * n,
1477
                        (2 * u - v) * w * mv * n**2 - 3 * v * mv * m * n,
1478
                        (2 * v - u) * w * mv * n**2 + 3 * u * mv * m * n,
1479
                        (w**2 * mu - u**2 * mv - v**2 * mv + u * v * mv) * n**2 + 3 * mu * m**2,
1480
                    ]
1481
                    m = -1 * m
1✔
1482
                    # inverse of the rotation matrix
1483
                    R_list_inv = [
1✔
1484
                        (u**2 * mv - v**2 * mv - w**2 * mu) * n**2 + 2 * w * mu * m * n + 3 * mu * m**2,
1485
                        (2 * v - u) * u * mv * n**2 - 4 * w * mu * m * n,
1486
                        2 * u * w * mu * n**2 + 2 * (2 * v - u) * mu * m * n,
1487
                        (2 * u - v) * v * mv * n**2 + 4 * w * mu * m * n,
1488
                        (v**2 * mv - u**2 * mv - w**2 * mu) * n**2 - 2 * w * mu * m * n + 3 * mu * m**2,
1489
                        2 * v * w * mu * n**2 - 2 * (2 * u - v) * mu * m * n,
1490
                        (2 * u - v) * w * mv * n**2 - 3 * v * mv * m * n,
1491
                        (2 * v - u) * w * mv * n**2 + 3 * u * mv * m * n,
1492
                        (w**2 * mu - u**2 * mv - v**2 * mv + u * v * mv) * n**2 + 3 * mu * m**2,
1493
                    ]
1494
                    m = -1 * m
1✔
1495
                    F = 3 * mu * m**2 + d * n**2
1✔
1496
                    all_list = R_list_inv + R_list + [F]
1✔
1497
                    # Compute the max common factors for the elements of the rotation matrix
1498
                    # and its inverse.
1499
                    com_fac = reduce(gcd, all_list)
1✔
1500
                    sigma = int(round((3 * mu * m**2 + d * n**2) / com_fac))
1✔
1501
                    if 1 < sigma <= cutoff:
1✔
1502
                        if sigma not in list(sigmas):
1✔
1503
                            if m == 0:
1✔
1504
                                angle = 180.0
×
1505
                            else:
1506
                                angle = 2 * np.arctan(n / m * np.sqrt(d / 3.0 / mu)) / np.pi * 180
1✔
1507
                            sigmas[sigma] = [angle]
1✔
1508
                        else:
1509
                            if m == 0:
1✔
1510
                                angle = 180.0
×
1511
                            else:
1512
                                angle = 2 * np.arctan(n / m * np.sqrt(d / 3.0 / mu)) / np.pi * 180
1✔
1513
                            if angle not in sigmas[sigma]:
1✔
1514
                                sigmas[sigma].append(angle)
1✔
1515
            if m_max == 0:
1✔
1516
                break
1✔
1517
        return sigmas
1✔
1518

1519
    @staticmethod
1✔
1520
    def enum_sigma_rho(cutoff, r_axis, ratio_alpha):
1✔
1521
        """
1522
        Find all possible sigma values and corresponding rotation angles
1523
        within a sigma value cutoff with known rotation axis in rhombohedral system.
1524
        The algorithm for this code is from reference, Acta Cryst, A45,505(1989).
1525

1526
        Args:
1527
            cutoff (int): the cutoff of sigma values.
1528
            r_axis (list of three integers, e.g. u, v, w
1529
                    or four integers, e.g. u, v, t, w):
1530
                    the rotation axis of the grain boundary, with the format of [u,v,w]
1531
                    or Weber indices [u, v, t, w].
1532
            ratio_alpha (list of two integers, e.g. mu, mv):
1533
                    mu/mv is the ratio of (1+2*cos(alpha))/cos(alpha) with rational number.
1534
                    If irrational, set ratio_alpha = None.
1535
        Returns:
1536
            sigmas (dict):
1537
                    dictionary with keys as the possible integer sigma values
1538
                    and values as list of the possible rotation angles to the
1539
                    corresponding sigma values.
1540
                    e.g. the format as
1541
                    {sigma1: [angle11,angle12,...], sigma2: [angle21, angle22,...],...}
1542
                    Note: the angles are the rotation angle of one grain respect to the
1543
                    other grain.
1544
                    When generate the microstructure of the grain boundary using these
1545
                    angles, you need to analyze the symmetry of the structure. Different
1546
                    angles may result in equivalent microstructures.
1547
        """
1548
        sigmas = {}
1✔
1549
        # transform four index notation to three index notation
1550
        if len(r_axis) == 4:
1✔
1551
            u1 = r_axis[0]
×
1552
            v1 = r_axis[1]
×
1553
            w1 = r_axis[3]
×
1554
            u = 2 * u1 + v1 + w1
×
1555
            v = v1 + w1 - u1
×
1556
            w = w1 - 2 * v1 - u1
×
1557
            r_axis = [u, v, w]
×
1558
        # make sure gcd(r_axis)==1
1559
        if reduce(gcd, r_axis) != 1:
1✔
1560
            r_axis = [int(round(x / reduce(gcd, r_axis))) for x in r_axis]
×
1561
        u, v, w = r_axis
1✔
1562
        # make sure mu, mv are coprime integers.
1563
        if ratio_alpha is None:
1✔
1564
            mu, mv = [1, 1]
×
1565
            if u + v + w != 0:
×
1566
                if u != v or u != w:
×
1567
                    raise RuntimeError(
×
1568
                        "For irrational ratio_alpha, CSL only exist for [1,1,1] or [u, v, -(u+v)] and m =0"
1569
                    )
1570
        else:
1571
            mu, mv = ratio_alpha
1✔
1572
            if gcd(mu, mv) != 1:
1✔
1573
                temp = gcd(mu, mv)
×
1574
                mu = int(round(mu / temp))
×
1575
                mv = int(round(mv / temp))
×
1576

1577
        # refer to the meaning of d in reference
1578
        d = (u**2 + v**2 + w**2) * (mu - 2 * mv) + 2 * mv * (v * w + w * u + u * v)
1✔
1579
        # Compute the max n we need to enumerate.
1580
        n_max = int(np.sqrt((cutoff * abs(4 * mu * (mu - 3 * mv))) / abs(d)))
1✔
1581

1582
        # Enumerate all possible n, m to give possible sigmas within the cutoff.
1583
        for n in range(1, n_max + 1):
1✔
1584
            if ratio_alpha is None and u + v + w == 0:
1✔
1585
                m_max = 0
×
1586
            else:
1587
                m_max = int(np.sqrt((cutoff * abs(4 * mu * (mu - 3 * mv)) - n**2 * d) / (mu)))
1✔
1588
            for m in range(0, m_max + 1):
1✔
1589
                if gcd(m, n) == 1 or m == 0:
1✔
1590
                    # construct the rotation matrix, refer to the reference
1591
                    R_list = [
1✔
1592
                        (mu - 2 * mv) * (u**2 - v**2 - w**2) * n**2
1593
                        + 2 * mv * (v - w) * m * n
1594
                        - 2 * mv * v * w * n**2
1595
                        + mu * m**2,
1596
                        2 * (mv * u * n * (w * n + u * n - m) - (mu - mv) * m * w * n + (mu - 2 * mv) * u * v * n**2),
1597
                        2 * (mv * u * n * (v * n + u * n + m) + (mu - mv) * m * v * n + (mu - 2 * mv) * w * u * n**2),
1598
                        2 * (mv * v * n * (w * n + v * n + m) + (mu - mv) * m * w * n + (mu - 2 * mv) * u * v * n**2),
1599
                        (mu - 2 * mv) * (v**2 - w**2 - u**2) * n**2
1600
                        + 2 * mv * (w - u) * m * n
1601
                        - 2 * mv * u * w * n**2
1602
                        + mu * m**2,
1603
                        2 * (mv * v * n * (v * n + u * n - m) - (mu - mv) * m * u * n + (mu - 2 * mv) * w * v * n**2),
1604
                        2 * (mv * w * n * (w * n + v * n - m) - (mu - mv) * m * v * n + (mu - 2 * mv) * w * u * n**2),
1605
                        2 * (mv * w * n * (w * n + u * n + m) + (mu - mv) * m * u * n + (mu - 2 * mv) * w * v * n**2),
1606
                        (mu - 2 * mv) * (w**2 - u**2 - v**2) * n**2
1607
                        + 2 * mv * (u - v) * m * n
1608
                        - 2 * mv * u * v * n**2
1609
                        + mu * m**2,
1610
                    ]
1611
                    m = -1 * m
1✔
1612
                    # inverse of the rotation matrix
1613
                    R_list_inv = [
1✔
1614
                        (mu - 2 * mv) * (u**2 - v**2 - w**2) * n**2
1615
                        + 2 * mv * (v - w) * m * n
1616
                        - 2 * mv * v * w * n**2
1617
                        + mu * m**2,
1618
                        2 * (mv * u * n * (w * n + u * n - m) - (mu - mv) * m * w * n + (mu - 2 * mv) * u * v * n**2),
1619
                        2 * (mv * u * n * (v * n + u * n + m) + (mu - mv) * m * v * n + (mu - 2 * mv) * w * u * n**2),
1620
                        2 * (mv * v * n * (w * n + v * n + m) + (mu - mv) * m * w * n + (mu - 2 * mv) * u * v * n**2),
1621
                        (mu - 2 * mv) * (v**2 - w**2 - u**2) * n**2
1622
                        + 2 * mv * (w - u) * m * n
1623
                        - 2 * mv * u * w * n**2
1624
                        + mu * m**2,
1625
                        2 * (mv * v * n * (v * n + u * n - m) - (mu - mv) * m * u * n + (mu - 2 * mv) * w * v * n**2),
1626
                        2 * (mv * w * n * (w * n + v * n - m) - (mu - mv) * m * v * n + (mu - 2 * mv) * w * u * n**2),
1627
                        2 * (mv * w * n * (w * n + u * n + m) + (mu - mv) * m * u * n + (mu - 2 * mv) * w * v * n**2),
1628
                        (mu - 2 * mv) * (w**2 - u**2 - v**2) * n**2
1629
                        + 2 * mv * (u - v) * m * n
1630
                        - 2 * mv * u * v * n**2
1631
                        + mu * m**2,
1632
                    ]
1633
                    m = -1 * m
1✔
1634
                    F = mu * m**2 + d * n**2
1✔
1635
                    all_list = R_list_inv + R_list + [F]
1✔
1636
                    # Compute the max common factors for the elements of the rotation matrix
1637
                    #  and its inverse.
1638
                    com_fac = reduce(gcd, all_list)
1✔
1639
                    sigma = int(round(abs(F / com_fac)))
1✔
1640
                    if 1 < sigma <= cutoff:
1✔
1641
                        if sigma not in list(sigmas):
1✔
1642
                            if m == 0:
1✔
1643
                                angle = 180.0
1✔
1644
                            else:
1645
                                angle = 2 * np.arctan(n / m * np.sqrt(d / mu)) / np.pi * 180
1✔
1646
                            sigmas[sigma] = [angle]
1✔
1647
                        else:
1648
                            if m == 0:
1✔
1649
                                angle = 180
1✔
1650
                            else:
1651
                                angle = 2 * np.arctan(n / m * np.sqrt(d / mu)) / np.pi * 180.0
×
1652
                            if angle not in sigmas[sigma]:
1✔
1653
                                sigmas[sigma].append(angle)
×
1654
            if m_max == 0:
1✔
1655
                break
×
1656
        return sigmas
1✔
1657

1658
    @staticmethod
1✔
1659
    def enum_sigma_tet(cutoff, r_axis, c2_a2_ratio):
1✔
1660
        """
1661
        Find all possible sigma values and corresponding rotation angles
1662
        within a sigma value cutoff with known rotation axis in tetragonal system.
1663
        The algorithm for this code is from reference, Acta Cryst, B46,117(1990)
1664

1665
        Args:
1666
            cutoff (int): the cutoff of sigma values.
1667
            r_axis (list of three integers, e.g. u, v, w):
1668
                    the rotation axis of the grain boundary, with the format of [u,v,w].
1669
            c2_a2_ratio (list of two integers, e.g. mu, mv):
1670
                    mu/mv is the square of the tetragonal axial ratio with rational number.
1671
                    if irrational, set c2_a2_ratio = None
1672
        Returns:
1673
            sigmas (dict):
1674
                    dictionary with keys as the possible integer sigma values
1675
                    and values as list of the possible rotation angles to the
1676
                    corresponding sigma values.
1677
                    e.g. the format as
1678
                    {sigma1: [angle11,angle12,...], sigma2: [angle21, angle22,...],...}
1679
                    Note: the angles are the rotation angle of one grain respect to the
1680
                    other grain.
1681
                    When generate the microstructure of the grain boundary using these
1682
                    angles, you need to analyze the symmetry of the structure. Different
1683
                    angles may result in equivalent microstructures.
1684
        """
1685
        sigmas = {}
1✔
1686
        # make sure gcd(r_axis)==1
1687
        if reduce(gcd, r_axis) != 1:
1✔
1688
            r_axis = [int(round(x / reduce(gcd, r_axis))) for x in r_axis]
×
1689

1690
        u, v, w = r_axis
1✔
1691

1692
        # make sure mu, mv are coprime integers.
1693
        if c2_a2_ratio is None:
1✔
1694
            mu, mv = [1, 1]
×
1695
            if w != 0:
×
1696
                if u != 0 or (v != 0):
×
1697
                    raise RuntimeError("For irrational c2/a2, CSL only exist for [0,0,1] or [u,v,0] and m = 0")
×
1698
        else:
1699
            mu, mv = c2_a2_ratio
1✔
1700
            if gcd(mu, mv) != 1:
1✔
1701
                temp = gcd(mu, mv)
×
1702
                mu = int(round(mu / temp))
×
1703
                mv = int(round(mv / temp))
×
1704

1705
        # refer to the meaning of d in reference
1706
        d = (u**2 + v**2) * mv + w**2 * mu
1✔
1707

1708
        # Compute the max n we need to enumerate.
1709
        n_max = int(np.sqrt((cutoff * 4 * mu * mv) / d))
1✔
1710

1711
        # Enumerate all possible n, m to give possible sigmas within the cutoff.
1712
        for n in range(1, n_max + 1):
1✔
1713
            if c2_a2_ratio is None and w == 0:
1✔
1714
                m_max = 0
×
1715
            else:
1716
                m_max = int(np.sqrt((cutoff * 4 * mu * mv - n**2 * d) / mu))
1✔
1717
            for m in range(0, m_max + 1):
1✔
1718
                if gcd(m, n) == 1 or m == 0:
1✔
1719
                    # construct the rotation matrix, refer to the reference
1720
                    R_list = [
1✔
1721
                        (u**2 * mv - v**2 * mv - w**2 * mu) * n**2 + mu * m**2,
1722
                        2 * v * u * mv * n**2 - 2 * w * mu * m * n,
1723
                        2 * u * w * mu * n**2 + 2 * v * mu * m * n,
1724
                        2 * u * v * mv * n**2 + 2 * w * mu * m * n,
1725
                        (v**2 * mv - u**2 * mv - w**2 * mu) * n**2 + mu * m**2,
1726
                        2 * v * w * mu * n**2 - 2 * u * mu * m * n,
1727
                        2 * u * w * mv * n**2 - 2 * v * mv * m * n,
1728
                        2 * v * w * mv * n**2 + 2 * u * mv * m * n,
1729
                        (w**2 * mu - u**2 * mv - v**2 * mv) * n**2 + mu * m**2,
1730
                    ]
1731
                    m = -1 * m
1✔
1732
                    # inverse of rotation matrix
1733
                    R_list_inv = [
1✔
1734
                        (u**2 * mv - v**2 * mv - w**2 * mu) * n**2 + mu * m**2,
1735
                        2 * v * u * mv * n**2 - 2 * w * mu * m * n,
1736
                        2 * u * w * mu * n**2 + 2 * v * mu * m * n,
1737
                        2 * u * v * mv * n**2 + 2 * w * mu * m * n,
1738
                        (v**2 * mv - u**2 * mv - w**2 * mu) * n**2 + mu * m**2,
1739
                        2 * v * w * mu * n**2 - 2 * u * mu * m * n,
1740
                        2 * u * w * mv * n**2 - 2 * v * mv * m * n,
1741
                        2 * v * w * mv * n**2 + 2 * u * mv * m * n,
1742
                        (w**2 * mu - u**2 * mv - v**2 * mv) * n**2 + mu * m**2,
1743
                    ]
1744
                    m = -1 * m
1✔
1745
                    F = mu * m**2 + d * n**2
1✔
1746
                    all_list = R_list + R_list_inv + [F]
1✔
1747
                    # Compute the max common factors for the elements of the rotation matrix
1748
                    #  and its inverse.
1749
                    com_fac = reduce(gcd, all_list)
1✔
1750
                    sigma = int(round((mu * m**2 + d * n**2) / com_fac))
1✔
1751
                    if 1 < sigma <= cutoff:
1✔
1752
                        if sigma not in list(sigmas):
1✔
1753
                            if m == 0:
1✔
1754
                                angle = 180.0
1✔
1755
                            else:
1756
                                angle = 2 * np.arctan(n / m * np.sqrt(d / mu)) / np.pi * 180
1✔
1757
                            sigmas[sigma] = [angle]
1✔
1758
                        else:
1759
                            if m == 0:
1✔
1760
                                angle = 180.0
1✔
1761
                            else:
1762
                                angle = 2 * np.arctan(n / m * np.sqrt(d / mu)) / np.pi * 180
1✔
1763
                            if angle not in sigmas[sigma]:
1✔
1764
                                sigmas[sigma].append(angle)
1✔
1765
            if m_max == 0:
1✔
1766
                break
×
1767

1768
        return sigmas
1✔
1769

1770
    @staticmethod
1✔
1771
    def enum_sigma_ort(cutoff, r_axis, c2_b2_a2_ratio):
1✔
1772
        """
1773
        Find all possible sigma values and corresponding rotation angles
1774
        within a sigma value cutoff with known rotation axis in orthorhombic system.
1775
        The algorithm for this code is from reference, Scipta Metallurgica 27, 291(1992)
1776

1777
        Args:
1778
            cutoff (int): the cutoff of sigma values.
1779
            r_axis (list of three integers, e.g. u, v, w):
1780
                    the rotation axis of the grain boundary, with the format of [u,v,w].
1781
            c2_b2_a2_ratio (list of three integers, e.g. mu,lambda, mv):
1782
                    mu:lam:mv is the square of the orthorhombic axial ratio with rational
1783
                    numbers. If irrational for one axis, set it to None.
1784
                    e.g. mu:lam:mv = c2,None,a2, means b2 is irrational.
1785
        Returns:
1786
            sigmas (dict):
1787
                    dictionary with keys as the possible integer sigma values
1788
                    and values as list of the possible rotation angles to the
1789
                    corresponding sigma values.
1790
                    e.g. the format as
1791
                    {sigma1: [angle11,angle12,...], sigma2: [angle21, angle22,...],...}
1792
                    Note: the angles are the rotation angle of one grain respect to the
1793
                    other grain.
1794
                    When generate the microstructure of the grain boundary using these
1795
                    angles, you need to analyze the symmetry of the structure. Different
1796
                    angles may result in equivalent microstructures.
1797
        """
1798
        sigmas = {}
1✔
1799
        # make sure gcd(r_axis)==1
1800
        if reduce(gcd, r_axis) != 1:
1✔
1801
            r_axis = [int(round(x / reduce(gcd, r_axis))) for x in r_axis]
×
1802

1803
        u, v, w = r_axis
1✔
1804
        # make sure mu, lambda, mv are coprime integers.
1805
        if None in c2_b2_a2_ratio:
1✔
1806
            mu, lam, mv = c2_b2_a2_ratio
×
1807
            non_none = [i for i in c2_b2_a2_ratio if i is not None]
×
1808
            if len(non_none) < 2:
×
1809
                raise RuntimeError("No CSL exist for two irrational numbers")
×
1810
            non1, non2 = non_none
×
1811
            if reduce(gcd, non_none) != 1:
×
1812
                temp = reduce(gcd, non_none)
×
1813
                non1 = int(round(non1 / temp))
×
1814
                non2 = int(round(non2 / temp))
×
1815
            if mu is None:
×
1816
                lam = non1
×
1817
                mv = non2
×
1818
                mu = 1
×
1819
                if w != 0:
×
1820
                    if u != 0 or (v != 0):
×
1821
                        raise RuntimeError("For irrational c2, CSL only exist for [0,0,1] or [u,v,0] and m = 0")
×
1822
            elif lam is None:
×
1823
                mu = non1
×
1824
                mv = non2
×
1825
                lam = 1
×
1826
                if v != 0:
×
1827
                    if u != 0 or (w != 0):
×
1828
                        raise RuntimeError("For irrational b2, CSL only exist for [0,1,0] or [u,0,w] and m = 0")
×
1829
            elif mv is None:
×
1830
                mu = non1
×
1831
                lam = non2
×
1832
                mv = 1
×
1833
                if u != 0:
×
1834
                    if w != 0 or (v != 0):
×
1835
                        raise RuntimeError("For irrational a2, CSL only exist for [1,0,0] or [0,v,w] and m = 0")
×
1836
        else:
1837
            mu, lam, mv = c2_b2_a2_ratio
1✔
1838
            if reduce(gcd, c2_b2_a2_ratio) != 1:
1✔
1839
                temp = reduce(gcd, c2_b2_a2_ratio)
×
1840
                mu = int(round(mu / temp))
×
1841
                mv = int(round(mv / temp))
×
1842
                lam = int(round(lam / temp))
×
1843
            if u == 0 and v == 0:
1✔
1844
                mu = 1
×
1845
            if u == 0 and w == 0:
1✔
1846
                lam = 1
×
1847
            if v == 0 and w == 0:
1✔
1848
                mv = 1
1✔
1849
        # refer to the meaning of d in reference
1850
        d = (mv * u**2 + lam * v**2) * mv + w**2 * mu * mv
1✔
1851

1852
        # Compute the max n we need to enumerate.
1853
        n_max = int(np.sqrt((cutoff * 4 * mu * mv * mv * lam) / d))
1✔
1854
        # Enumerate all possible n, m to give possible sigmas within the cutoff.
1855
        for n in range(1, n_max + 1):
1✔
1856
            mu_temp, lam_temp, mv_temp = c2_b2_a2_ratio
1✔
1857
            if (mu_temp is None and w == 0) or (lam_temp is None and v == 0) or (mv_temp is None and u == 0):
1✔
1858
                m_max = 0
×
1859
            else:
1860
                m_max = int(np.sqrt((cutoff * 4 * mu * mv * lam * mv - n**2 * d) / mu / lam))
1✔
1861
            for m in range(0, m_max + 1):
1✔
1862
                if gcd(m, n) == 1 or m == 0:
1✔
1863
                    # construct the rotation matrix, refer to the reference
1864
                    R_list = [
1✔
1865
                        (u**2 * mv * mv - lam * v**2 * mv - w**2 * mu * mv) * n**2 + lam * mu * m**2,
1866
                        2 * lam * (v * u * mv * n**2 - w * mu * m * n),
1867
                        2 * mu * (u * w * mv * n**2 + v * lam * m * n),
1868
                        2 * mv * (u * v * mv * n**2 + w * mu * m * n),
1869
                        (v**2 * mv * lam - u**2 * mv * mv - w**2 * mu * mv) * n**2 + lam * mu * m**2,
1870
                        2 * mv * mu * (v * w * n**2 - u * m * n),
1871
                        2 * mv * (u * w * mv * n**2 - v * lam * m * n),
1872
                        2 * lam * mv * (v * w * n**2 + u * m * n),
1873
                        (w**2 * mu * mv - u**2 * mv * mv - v**2 * mv * lam) * n**2 + lam * mu * m**2,
1874
                    ]
1875
                    m = -1 * m
1✔
1876
                    # inverse of rotation matrix
1877
                    R_list_inv = [
1✔
1878
                        (u**2 * mv * mv - lam * v**2 * mv - w**2 * mu * mv) * n**2 + lam * mu * m**2,
1879
                        2 * lam * (v * u * mv * n**2 - w * mu * m * n),
1880
                        2 * mu * (u * w * mv * n**2 + v * lam * m * n),
1881
                        2 * mv * (u * v * mv * n**2 + w * mu * m * n),
1882
                        (v**2 * mv * lam - u**2 * mv * mv - w**2 * mu * mv) * n**2 + lam * mu * m**2,
1883
                        2 * mv * mu * (v * w * n**2 - u * m * n),
1884
                        2 * mv * (u * w * mv * n**2 - v * lam * m * n),
1885
                        2 * lam * mv * (v * w * n**2 + u * m * n),
1886
                        (w**2 * mu * mv - u**2 * mv * mv - v**2 * mv * lam) * n**2 + lam * mu * m**2,
1887
                    ]
1888
                    m = -1 * m
1✔
1889
                    F = mu * lam * m**2 + d * n**2
1✔
1890
                    all_list = R_list + R_list_inv + [F]
1✔
1891
                    # Compute the max common factors for the elements of the rotation matrix
1892
                    #  and its inverse.
1893
                    com_fac = reduce(gcd, all_list)
1✔
1894
                    sigma = int(round((mu * lam * m**2 + d * n**2) / com_fac))
1✔
1895
                    if 1 < sigma <= cutoff:
1✔
1896
                        if sigma not in list(sigmas):
1✔
1897
                            if m == 0:
1✔
1898
                                angle = 180.0
×
1899
                            else:
1900
                                angle = 2 * np.arctan(n / m * np.sqrt(d / mu / lam)) / np.pi * 180
1✔
1901
                            sigmas[sigma] = [angle]
1✔
1902
                        else:
1903
                            if m == 0:
1✔
1904
                                angle = 180.0
×
1905
                            else:
1906
                                angle = 2 * np.arctan(n / m * np.sqrt(d / mu / lam)) / np.pi * 180
1✔
1907
                            if angle not in sigmas[sigma]:
1✔
1908
                                sigmas[sigma].append(angle)
1✔
1909
            if m_max == 0:
1✔
1910
                break
1✔
1911

1912
        return sigmas
1✔
1913

1914
    @staticmethod
1✔
1915
    def enum_possible_plane_cubic(plane_cutoff, r_axis, r_angle):
1✔
1916
        """
1917
        Find all possible plane combinations for GBs given a rotation axis and angle for
1918
        cubic system, and classify them to different categories, including 'Twist',
1919
        'Symmetric tilt', 'Normal tilt', 'Mixed' GBs.
1920

1921
        Args:
1922
            plane_cutoff (int): the cutoff of plane miller index.
1923
            r_axis (list of three integers, e.g. u, v, w):
1924
                    the rotation axis of the grain boundary, with the format of [u,v,w].
1925
            r_angle (float): rotation angle of the GBs.
1926
        Returns:
1927
            all_combinations (dict):
1928
                    dictionary with keys as GB type, e.g. 'Twist','Symmetric tilt',etc.
1929
                    and values as the combination of the two plane miller index
1930
                     (GB plane and joining plane).
1931
        """
1932
        all_combinations = {}
1✔
1933
        all_combinations["Symmetric tilt"] = []
1✔
1934
        all_combinations["Twist"] = []
1✔
1935
        all_combinations["Normal tilt"] = []
1✔
1936
        all_combinations["Mixed"] = []
1✔
1937
        sym_plane = symm_group_cubic([[1, 0, 0], [1, 1, 0]])
1✔
1938
        j = np.arange(0, plane_cutoff + 1)
1✔
1939
        combination = []
1✔
1940
        for idx in itertools.product(j, repeat=3):
1✔
1941
            if sum(abs(np.array(idx))) != 0:
1✔
1942
                combination.append(list(idx))
1✔
1943
            if len(np.nonzero(idx)[0]) == 3:
1✔
1944
                for i1 in range(3):
1✔
1945
                    new_i = list(idx).copy()
1✔
1946
                    new_i[i1] = -1 * new_i[i1]
1✔
1947
                    combination.append(new_i)
1✔
1948
            elif len(np.nonzero(idx)[0]) == 2:
1✔
1949
                new_i = list(idx).copy()
1✔
1950
                new_i[np.nonzero(idx)[0][0]] = -1 * new_i[np.nonzero(idx)[0][0]]
1✔
1951
                combination.append(new_i)
1✔
1952
        miller = np.array(combination)
1✔
1953
        miller = miller[np.argsort(np.linalg.norm(miller, axis=1))]
1✔
1954
        for val in miller:
1✔
1955
            if reduce(gcd, val) == 1:
1✔
1956
                matrix = GrainBoundaryGenerator.get_trans_mat(r_axis, r_angle, surface=val, quick_gen=True)
1✔
1957
                vec = np.cross(matrix[1][0], matrix[1][1])
1✔
1958
                miller2 = GrainBoundaryGenerator.vec_to_surface(vec)
1✔
1959
                if np.all(np.abs(np.array(miller2)) <= plane_cutoff):
1✔
1960
                    cos_1 = abs(np.dot(val, r_axis) / np.linalg.norm(val) / np.linalg.norm(r_axis))
1✔
1961
                    if 1 - cos_1 < 1.0e-5:
1✔
1962
                        all_combinations["Twist"].append([list(val), miller2])
1✔
1963
                    elif cos_1 < 1.0e-8:
1✔
1964
                        sym_tilt = False
1✔
1965
                        if np.sum(np.abs(val)) == np.sum(np.abs(miller2)):
1✔
1966
                            ave = (np.array(val) + np.array(miller2)) / 2
1✔
1967
                            ave1 = (np.array(val) - np.array(miller2)) / 2
1✔
1968
                            for plane in sym_plane:
1✔
1969
                                cos_2 = abs(np.dot(ave, plane) / np.linalg.norm(ave) / np.linalg.norm(plane))
1✔
1970
                                cos_3 = abs(np.dot(ave1, plane) / np.linalg.norm(ave1) / np.linalg.norm(plane))
1✔
1971
                                if 1 - cos_2 < 1.0e-5 or 1 - cos_3 < 1.0e-5:
1✔
1972
                                    all_combinations["Symmetric tilt"].append([list(val), miller2])
1✔
1973
                                    sym_tilt = True
1✔
1974
                                    break
1✔
1975
                        if not sym_tilt:
1✔
1976
                            all_combinations["Normal tilt"].append([list(val), miller2])
1✔
1977
                    else:
1978
                        all_combinations["Mixed"].append([list(val), miller2])
1✔
1979
        return all_combinations
1✔
1980

1981
    @staticmethod
1✔
1982
    def get_rotation_angle_from_sigma(sigma, r_axis, lat_type="C", ratio=None):
1✔
1983
        """
1984
        Find all possible rotation angle for the given sigma value.
1985

1986
        Args:
1987
            sigma (int):
1988
                    sigma value provided
1989
            r_axis (list of three integers, e.g. u, v, w
1990
                    or four integers, e.g. u, v, t, w for hex/rho system only):
1991
                    the rotation axis of the grain boundary.
1992
            lat_type ( one character):
1993
                    'c' or 'C': cubic system
1994
                     't' or 'T': tetragonal system
1995
                     'o' or 'O': orthorhombic system
1996
                     'h' or 'H': hexagonal system
1997
                     'r' or 'R': rhombohedral system
1998
                     default to cubic system
1999
            ratio (list of integers):
2000
                    lattice axial ratio.
2001
                    For cubic system, ratio is not needed.
2002
                    For tetragonal system, ratio = [mu, mv], list of two integers,
2003
                    that is, mu/mv = c2/a2. If it is irrational, set it to none.
2004
                    For orthorhombic system, ratio = [mu, lam, mv], list of three integers,
2005
                    that is, mu:lam:mv = c2:b2:a2. If irrational for one axis, set it to None.
2006
                    e.g. mu:lam:mv = c2,None,a2, means b2 is irrational.
2007
                    For rhombohedral system, ratio = [mu, mv], list of two integers,
2008
                    that is, mu/mv is the ratio of (1+2*cos(alpha)/cos(alpha).
2009
                    If irrational, set it to None.
2010
                    For hexagonal system, ratio = [mu, mv], list of two integers,
2011
                    that is, mu/mv = c2/a2. If it is irrational, set it to none.
2012

2013
        Returns:
2014
            rotation_angles corresponding to the provided sigma value.
2015
            If the sigma value is not correct, return the rotation angle corresponding
2016
            to the correct possible sigma value right smaller than the wrong sigma value provided.
2017
        """
2018
        if lat_type.lower() == "c":
1✔
2019
            logger.info("Make sure this is for cubic system")
×
2020
            sigma_dict = GrainBoundaryGenerator.enum_sigma_cubic(cutoff=sigma, r_axis=r_axis)
×
2021
        elif lat_type.lower() == "t":
1✔
2022
            logger.info("Make sure this is for tetragonal system")
×
2023
            if ratio is None:
×
2024
                logger.info("Make sure this is for irrational c2/a2 ratio")
×
2025
            elif len(ratio) != 2:
×
2026
                raise RuntimeError("Tetragonal system needs correct c2/a2 ratio")
×
2027
            sigma_dict = GrainBoundaryGenerator.enum_sigma_tet(cutoff=sigma, r_axis=r_axis, c2_a2_ratio=ratio)
×
2028
        elif lat_type.lower() == "o":
1✔
2029
            logger.info("Make sure this is for orthorhombic system")
1✔
2030
            if len(ratio) != 3:
1✔
2031
                raise RuntimeError("Orthorhombic system needs correct c2:b2:a2 ratio")
×
2032
            sigma_dict = GrainBoundaryGenerator.enum_sigma_ort(cutoff=sigma, r_axis=r_axis, c2_b2_a2_ratio=ratio)
1✔
2033
        elif lat_type.lower() == "h":
×
2034
            logger.info("Make sure this is for hexagonal system")
×
2035
            if ratio is None:
×
2036
                logger.info("Make sure this is for irrational c2/a2 ratio")
×
2037
            elif len(ratio) != 2:
×
2038
                raise RuntimeError("Hexagonal system needs correct c2/a2 ratio")
×
2039
            sigma_dict = GrainBoundaryGenerator.enum_sigma_hex(cutoff=sigma, r_axis=r_axis, c2_a2_ratio=ratio)
×
2040
        elif lat_type.lower() == "r":
×
2041
            logger.info("Make sure this is for rhombohedral system")
×
2042
            if ratio is None:
×
2043
                logger.info("Make sure this is for irrational (1+2*cos(alpha)/cos(alpha) ratio")
×
2044
            elif len(ratio) != 2:
×
2045
                raise RuntimeError("Rhombohedral system needs correct (1+2*cos(alpha)/cos(alpha) ratio")
×
2046
            sigma_dict = GrainBoundaryGenerator.enum_sigma_rho(cutoff=sigma, r_axis=r_axis, ratio_alpha=ratio)
×
2047
        else:
2048
            raise RuntimeError("Lattice type not implemented")
×
2049

2050
        sigmas = list(sigma_dict)
1✔
2051
        if not sigmas:
1✔
2052
            raise RuntimeError("This is a wriong sigma value, and no sigma exists smaller than this value.")
×
2053
        if sigma in sigmas:
1✔
2054
            rotation_angles = sigma_dict[sigma]
1✔
2055
        else:
2056
            sigmas.sort()
1✔
2057
            warnings.warn(
1✔
2058
                "This is not the possible sigma value according to the rotation axis!"
2059
                "The nearest neighbor sigma and its corresponding angle are returned"
2060
            )
2061
            rotation_angles = sigma_dict[sigmas[-1]]
1✔
2062
        rotation_angles.sort()
1✔
2063
        return rotation_angles
1✔
2064

2065
    @staticmethod
1✔
2066
    def slab_from_csl(csl, surface, normal, trans_cry, max_search=20, quick_gen=False):
1✔
2067
        """
2068
        By linear operation of csl lattice vectors to get the best corresponding
2069
        slab lattice. That is the area of a,b vectors (within the surface plane)
2070
        is the smallest, the c vector first, has shortest length perpendicular
2071
        to surface [h,k,l], second, has shortest length itself.
2072

2073
        Args:
2074
            csl (3 by 3 integer array):
2075
                    input csl lattice.
2076
            surface (list of three integers, e.g. h, k, l):
2077
                    the miller index of the surface, with the format of [h,k,l]
2078
            normal (logic):
2079
                    determine if the c vector needs to perpendicular to surface
2080
            trans_cry (3 by 3 array):
2081
                    transform matrix from crystal system to orthogonal system
2082
            max_search (int): max search for the GB lattice vectors that give the smallest GB
2083
                lattice. If normal is true, also max search the GB c vector that perpendicular
2084
                to the plane.
2085
            quick_gen (bool): whether to quickly generate a supercell, no need to find the smallest
2086
                cell if set to true.
2087

2088
        Returns:
2089
            t_matrix: a slab lattice ( 3 by 3 integer array):
2090
        """
2091
        # set the transform matrix in real space
2092
        trans = trans_cry
1✔
2093
        # transform matrix in reciprocal space
2094
        ctrans = np.linalg.inv(trans.T)
1✔
2095

2096
        t_matrix = csl.copy()
1✔
2097
        # vectors constructed from csl that perpendicular to surface
2098
        ab_vector = []
1✔
2099
        # obtain the miller index of surface in terms of csl.
2100
        miller = np.matmul(surface, csl.T)
1✔
2101
        if reduce(gcd, miller) != 1:
1✔
2102
            miller = [int(round(x / reduce(gcd, miller))) for x in miller]
1✔
2103
        miller_nonzero = []
1✔
2104
        # quickly generate a supercell, normal is not work in this way
2105
        if quick_gen:
1✔
2106
            scale_factor = []
1✔
2107
            eye = np.eye(3, dtype=int)
1✔
2108
            for i, j in enumerate(miller):
1✔
2109
                if j == 0:
1✔
2110
                    scale_factor.append(eye[i])
1✔
2111
                else:
2112
                    miller_nonzero.append(i)
1✔
2113
            if len(scale_factor) < 2:
1✔
2114
                index_len = len(miller_nonzero)
1✔
2115
                for i in range(index_len):
1✔
2116
                    for j in range(i + 1, index_len):
1✔
2117
                        lcm_miller = lcm(miller[miller_nonzero[i]], miller[miller_nonzero[j]])
1✔
2118
                        l = [0, 0, 0]
1✔
2119
                        l[miller_nonzero[i]] = -int(round(lcm_miller / miller[miller_nonzero[i]]))
1✔
2120
                        l[miller_nonzero[j]] = int(round(lcm_miller / miller[miller_nonzero[j]]))
1✔
2121
                        scale_factor.append(l)
1✔
2122
                        if len(scale_factor) == 2:
1✔
2123
                            break
1✔
2124
            t_matrix[0] = np.array(np.dot(scale_factor[0], csl))
1✔
2125
            t_matrix[1] = np.array(np.dot(scale_factor[1], csl))
1✔
2126
            t_matrix[2] = csl[miller_nonzero[0]]
1✔
2127
            if abs(np.linalg.det(t_matrix)) > 1000:
1✔
2128
                warnings.warn("Too large matrix. Suggest to use quick_gen=False")
×
2129
            return t_matrix
1✔
2130

2131
        for i, j in enumerate(miller):
1✔
2132
            if j == 0:
1✔
2133
                ab_vector.append(csl[i])
1✔
2134
            else:
2135
                c_index = i
1✔
2136
                miller_nonzero.append(j)
1✔
2137

2138
        if len(miller_nonzero) > 1:
1✔
2139
            t_matrix[2] = csl[c_index]
1✔
2140
            index_len = len(miller_nonzero)
1✔
2141
            lcm_miller = []
1✔
2142
            for i in range(index_len):
1✔
2143
                for j in range(i + 1, index_len):
1✔
2144
                    com_gcd = gcd(miller_nonzero[i], miller_nonzero[j])
1✔
2145
                    mil1 = int(round(miller_nonzero[i] / com_gcd))
1✔
2146
                    mil2 = int(round(miller_nonzero[j] / com_gcd))
1✔
2147
                    lcm_miller.append(max(abs(mil1), abs(mil2)))
1✔
2148
            lcm_sorted = sorted(lcm_miller)
1✔
2149
            if index_len == 2:
1✔
2150
                max_j = lcm_sorted[0]
1✔
2151
            else:
2152
                max_j = lcm_sorted[1]
1✔
2153
        else:
2154
            if not normal:
1✔
2155
                t_matrix[0] = ab_vector[0]
1✔
2156
                t_matrix[1] = ab_vector[1]
1✔
2157
                t_matrix[2] = csl[c_index]
1✔
2158
                return t_matrix
1✔
2159
            max_j = abs(miller_nonzero[0])
×
2160
        max_j = min(max_j, max_search)
1✔
2161
        # area of a, b vectors
2162
        area = None
1✔
2163
        # length of c vector
2164
        c_norm = np.linalg.norm(np.matmul(t_matrix[2], trans))
1✔
2165
        # c vector length along the direction perpendicular to surface
2166
        c_length = np.abs(np.dot(t_matrix[2], surface))
1✔
2167
        # check if the init c vector perpendicular to the surface
2168
        if normal:
1✔
2169
            c_cross = np.cross(np.matmul(t_matrix[2], trans), np.matmul(surface, ctrans))
1✔
2170
            normal_init = np.linalg.norm(c_cross) < 1e-8
1✔
2171

2172
        j = np.arange(0, max_j + 1)
1✔
2173
        combination = []
1✔
2174
        for i in itertools.product(j, repeat=3):
1✔
2175
            if sum(abs(np.array(i))) != 0:
1✔
2176
                combination.append(list(i))
1✔
2177
            if len(np.nonzero(i)[0]) == 3:
1✔
2178
                for i1 in range(3):
1✔
2179
                    new_i = list(i).copy()
1✔
2180
                    new_i[i1] = -1 * new_i[i1]
1✔
2181
                    combination.append(new_i)
1✔
2182
            elif len(np.nonzero(i)[0]) == 2:
1✔
2183
                new_i = list(i).copy()
1✔
2184
                new_i[np.nonzero(i)[0][0]] = -1 * new_i[np.nonzero(i)[0][0]]
1✔
2185
                combination.append(new_i)
1✔
2186
        for i in combination:
1✔
2187
            if reduce(gcd, i) == 1:
1✔
2188
                temp = np.dot(np.array(i), csl)
1✔
2189
                if abs(np.dot(temp, surface) - 0) < 1.0e-8:
1✔
2190
                    ab_vector.append(temp)
1✔
2191
                else:
2192
                    # c vector length along the direction perpendicular to surface
2193
                    c_len_temp = np.abs(np.dot(temp, surface))
1✔
2194
                    # c vector length itself
2195
                    c_norm_temp = np.linalg.norm(np.matmul(temp, trans))
1✔
2196
                    if normal:
1✔
2197
                        c_cross = np.cross(np.matmul(temp, trans), np.matmul(surface, ctrans))
1✔
2198
                        if np.linalg.norm(c_cross) < 1.0e-8:
1✔
2199
                            if normal_init:
1✔
2200
                                if c_norm_temp < c_norm:
×
2201
                                    t_matrix[2] = temp
×
2202
                                    c_norm = c_norm_temp
×
2203
                            else:
2204
                                c_norm = c_norm_temp
1✔
2205
                                normal_init = True
1✔
2206
                                t_matrix[2] = temp
1✔
2207
                    else:
2208
                        if c_len_temp < c_length or (abs(c_len_temp - c_length) < 1.0e-8 and c_norm_temp < c_norm):
1✔
2209
                            t_matrix[2] = temp
1✔
2210
                            c_norm = c_norm_temp
1✔
2211
                            c_length = c_len_temp
1✔
2212

2213
        if normal and (not normal_init):
1✔
2214
            logger.info("Did not find the perpendicular c vector, increase max_j")
×
2215
            while not normal_init:
×
2216
                if max_j == max_search:
×
2217
                    warnings.warn("Cannot find the perpendicular c vector, please increase max_search")
×
2218
                    break
×
2219
                max_j = 3 * max_j
×
2220
                max_j = min(max_j, max_search)
×
2221
                j = np.arange(0, max_j + 1)
×
2222
                combination = []
×
2223
                for i in itertools.product(j, repeat=3):
×
2224
                    if sum(abs(np.array(i))) != 0:
×
2225
                        combination.append(list(i))
×
2226
                    if len(np.nonzero(i)[0]) == 3:
×
2227
                        for i1 in range(3):
×
2228
                            new_i = list(i).copy()
×
2229
                            new_i[i1] = -1 * new_i[i1]
×
2230
                            combination.append(new_i)
×
2231
                    elif len(np.nonzero(i)[0]) == 2:
×
2232
                        new_i = list(i).copy()
×
2233
                        new_i[np.nonzero(i)[0][0]] = -1 * new_i[np.nonzero(i)[0][0]]
×
2234
                        combination.append(new_i)
×
2235
                for i in combination:
×
2236
                    if reduce(gcd, i) == 1:
×
2237
                        temp = np.dot(np.array(i), csl)
×
2238
                        if abs(np.dot(temp, surface) - 0) > 1.0e-8:
×
2239
                            c_cross = np.cross(np.matmul(temp, trans), np.matmul(surface, ctrans))
×
2240
                            if np.linalg.norm(c_cross) < 1.0e-8:
×
2241
                                # c vector length itself
2242
                                c_norm_temp = np.linalg.norm(np.matmul(temp, trans))
×
2243
                                if normal_init:
×
2244
                                    if c_norm_temp < c_norm:
×
2245
                                        t_matrix[2] = temp
×
2246
                                        c_norm = c_norm_temp
×
2247
                                else:
2248
                                    c_norm = c_norm_temp
×
2249
                                    normal_init = True
×
2250
                                    t_matrix[2] = temp
×
2251
                if normal_init:
×
2252
                    logger.info("Found perpendicular c vector")
×
2253

2254
        # find the best a, b vectors with their formed area smallest and average norm of a,b smallest.
2255
        for i in itertools.combinations(ab_vector, 2):
1✔
2256
            area_temp = np.linalg.norm(np.cross(np.matmul(i[0], trans), np.matmul(i[1], trans)))
1✔
2257
            if abs(area_temp - 0) > 1.0e-8:
1✔
2258
                ab_norm_temp = np.linalg.norm(np.matmul(i[0], trans)) + np.linalg.norm(np.matmul(i[1], trans))
1✔
2259
                if area is None:
1✔
2260
                    area = area_temp
1✔
2261
                    ab_norm = ab_norm_temp
1✔
2262
                    t_matrix[0] = i[0]
1✔
2263
                    t_matrix[1] = i[1]
1✔
2264
                elif area_temp < area:
1✔
2265
                    t_matrix[0] = i[0]
1✔
2266
                    t_matrix[1] = i[1]
1✔
2267
                    area = area_temp
1✔
2268
                    ab_norm = ab_norm_temp
1✔
2269
                elif abs(area - area_temp) < 1.0e-8 and ab_norm_temp < ab_norm:
1✔
2270
                    t_matrix[0] = i[0]
1✔
2271
                    t_matrix[1] = i[1]
1✔
2272
                    area = area_temp
1✔
2273
                    ab_norm = ab_norm_temp
1✔
2274

2275
        # make sure we have a left-handed crystallographic system
2276
        if np.linalg.det(np.matmul(t_matrix, trans)) < 0:
1✔
2277
            t_matrix *= -1
1✔
2278

2279
        if normal and abs(np.linalg.det(t_matrix)) > 1000:
1✔
2280
            warnings.warn("Too large matrix. Suggest to use Normal=False")
1✔
2281
        return t_matrix
1✔
2282

2283
    @staticmethod
1✔
2284
    def reduce_mat(mat, mag, r_matrix):
1✔
2285
        """
2286
        Reduce integer array mat's determinant mag times by linear combination
2287
        of its row vectors, so that the new array after rotation (r_matrix) is
2288
        still an integer array
2289

2290
        Args:
2291
            mat (3 by 3 array): input matrix
2292
            mag (int): reduce times for the determinant
2293
            r_matrix (3 by 3 array): rotation matrix
2294
        Return:
2295
            the reduced integer array
2296
        """
2297
        max_j = abs(int(round(np.linalg.det(mat) / mag)))
1✔
2298
        reduced = False
1✔
2299
        for h in range(3):
1✔
2300
            k = h + 1 if h + 1 < 3 else abs(2 - h)
1✔
2301
            l = h + 2 if h + 2 < 3 else abs(1 - h)
1✔
2302
            j = np.arange(-max_j, max_j + 1)
1✔
2303
            for j1, j2 in itertools.product(j, repeat=2):
1✔
2304
                temp = mat[h] + j1 * mat[k] + j2 * mat[l]
1✔
2305
                if all(np.round(x, 5).is_integer() for x in list(temp / mag)):
1✔
2306
                    mat_copy = mat.copy()
1✔
2307
                    mat_copy[h] = np.array([int(round(ele / mag)) for ele in temp])
1✔
2308
                    new_mat = np.dot(mat_copy, np.linalg.inv(r_matrix.T))
1✔
2309
                    if all(np.round(x, 5).is_integer() for x in list(np.ravel(new_mat))):
1✔
2310
                        reduced = True
1✔
2311
                        mat[h] = np.array([int(round(ele / mag)) for ele in temp])
1✔
2312
                        break
1✔
2313
            if reduced:
1✔
2314
                break
1✔
2315

2316
        if not reduced:
1✔
2317
            warnings.warn("Matrix reduction not performed, may lead to non-primitive gb cell.")
×
2318
        return mat
1✔
2319

2320
    @staticmethod
1✔
2321
    def vec_to_surface(vec):
1✔
2322
        """
2323
        Transform a float vector to a surface miller index with integers.
2324

2325
        Args:
2326
            vec (1 by 3 array float vector): input float vector
2327
        Return:
2328
            the surface miller index of the input vector.
2329
        """
2330
        miller = [None] * 3
1✔
2331
        index = []
1✔
2332
        for idx, value in enumerate(vec):
1✔
2333
            if abs(value) < 1.0e-8:
1✔
2334
                miller[idx] = 0
1✔
2335
            else:
2336
                index.append(idx)
1✔
2337
        if len(index) == 1:
1✔
2338
            miller[index[0]] = 1
1✔
2339
        else:
2340
            min_index = np.argmin([i for i in vec if i != 0])
1✔
2341
            true_index = index[min_index]
1✔
2342
            index.pop(min_index)
1✔
2343
            frac = []
1✔
2344
            for value in index:
1✔
2345
                frac.append(Fraction(vec[value] / vec[true_index]).limit_denominator(100))
1✔
2346
            if len(index) == 1:
1✔
2347
                miller[true_index] = frac[0].denominator
1✔
2348
                miller[index[0]] = frac[0].numerator
1✔
2349
            else:
2350
                com_lcm = lcm(frac[0].denominator, frac[1].denominator)
1✔
2351
                miller[true_index] = com_lcm
1✔
2352
                miller[index[0]] = frac[0].numerator * int(round(com_lcm / frac[0].denominator))
1✔
2353
                miller[index[1]] = frac[1].numerator * int(round(com_lcm / frac[1].denominator))
1✔
2354
        return miller
1✔
2355

2356

2357
def fix_pbc(structure, matrix=None):
1✔
2358
    """
2359
    Set all frac_coords of the input structure within [0,1].
2360

2361
    Args:
2362
        structure (pymatgen structure object):
2363
            input structure
2364
        matrix (lattice matrix, 3 by 3 array/matrix)
2365
            new structure's lattice matrix, if none, use
2366
            input structure's matrix
2367

2368
    Return:
2369
        new structure with fixed frac_coords and lattice matrix
2370
    """
2371

2372
    spec = []
1✔
2373
    coords = []
1✔
2374
    if matrix is None:
1✔
2375
        latte = Lattice(structure.lattice.matrix)
1✔
2376
    else:
2377
        latte = Lattice(matrix)
1✔
2378

2379
    for site in structure:
1✔
2380
        spec.append(site.specie)
1✔
2381
        coord = np.array(site.frac_coords)
1✔
2382
        for i in range(3):
1✔
2383
            coord[i] -= floor(coord[i])
1✔
2384
            if np.allclose(coord[i], 1):
1✔
2385
                coord[i] = 0
1✔
2386
            elif np.allclose(coord[i], 0):
1✔
2387
                coord[i] = 0
1✔
2388
            else:
2389
                coord[i] = round(coord[i], 7)
1✔
2390
        coords.append(coord)
1✔
2391

2392
    return Structure(latte, spec, coords, site_properties=structure.site_properties)
1✔
2393

2394

2395
def symm_group_cubic(mat):
1✔
2396
    """
2397
     obtain cubic symmetric equivalents of the list of vectors.
2398

2399
    Args:
2400
        matrix (lattice matrix, n by 3 array/matrix)
2401

2402
    Return:
2403
        cubic symmetric equivalents of the list of vectors.
2404
    """
2405
    sym_group = np.zeros([24, 3, 3])
1✔
2406
    sym_group[0, :] = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
1✔
2407
    sym_group[1, :] = [[1, 0, 0], [0, -1, 0], [0, 0, -1]]
1✔
2408
    sym_group[2, :] = [[-1, 0, 0], [0, 1, 0], [0, 0, -1]]
1✔
2409
    sym_group[3, :] = [[-1, 0, 0], [0, -1, 0], [0, 0, 1]]
1✔
2410
    sym_group[4, :] = [[0, -1, 0], [-1, 0, 0], [0, 0, -1]]
1✔
2411
    sym_group[5, :] = [[0, -1, 0], [1, 0, 0], [0, 0, 1]]
1✔
2412
    sym_group[6, :] = [[0, 1, 0], [-1, 0, 0], [0, 0, 1]]
1✔
2413
    sym_group[7, :] = [[0, 1, 0], [1, 0, 0], [0, 0, -1]]
1✔
2414
    sym_group[8, :] = [[-1, 0, 0], [0, 0, -1], [0, -1, 0]]
1✔
2415
    sym_group[9, :] = [[-1, 0, 0], [0, 0, 1], [0, 1, 0]]
1✔
2416
    sym_group[10, :] = [[1, 0, 0], [0, 0, -1], [0, 1, 0]]
1✔
2417
    sym_group[11, :] = [[1, 0, 0], [0, 0, 1], [0, -1, 0]]
1✔
2418
    sym_group[12, :] = [[0, 1, 0], [0, 0, 1], [1, 0, 0]]
1✔
2419
    sym_group[13, :] = [[0, 1, 0], [0, 0, -1], [-1, 0, 0]]
1✔
2420
    sym_group[14, :] = [[0, -1, 0], [0, 0, 1], [-1, 0, 0]]
1✔
2421
    sym_group[15, :] = [[0, -1, 0], [0, 0, -1], [1, 0, 0]]
1✔
2422
    sym_group[16, :] = [[0, 0, 1], [1, 0, 0], [0, 1, 0]]
1✔
2423
    sym_group[17, :] = [[0, 0, 1], [-1, 0, 0], [0, -1, 0]]
1✔
2424
    sym_group[18, :] = [[0, 0, -1], [1, 0, 0], [0, -1, 0]]
1✔
2425
    sym_group[19, :] = [[0, 0, -1], [-1, 0, 0], [0, 1, 0]]
1✔
2426
    sym_group[20, :] = [[0, 0, -1], [0, -1, 0], [-1, 0, 0]]
1✔
2427
    sym_group[21, :] = [[0, 0, -1], [0, 1, 0], [1, 0, 0]]
1✔
2428
    sym_group[22, :] = [[0, 0, 1], [0, -1, 0], [1, 0, 0]]
1✔
2429
    sym_group[23, :] = [[0, 0, 1], [0, 1, 0], [-1, 0, 0]]
1✔
2430

2431
    mat = np.atleast_2d(mat)
1✔
2432
    all_vectors = []
1✔
2433
    for sym in sym_group:
1✔
2434
        for vec in mat:
1✔
2435
            all_vectors.append(np.dot(sym, vec))
1✔
2436
    return np.unique(np.array(all_vectors), axis=0)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc