• 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

96.66
/pymatgen/analysis/piezo_sensitivity.py
1
"""
2
Piezo sensitivity analysis module.
3
"""
4

5
from __future__ import annotations
1✔
6

7
import warnings
1✔
8

9
import numpy as np
1✔
10
from monty.dev import requires
1✔
11

12
from pymatgen.core.structure import Structure
1✔
13
from pymatgen.core.tensors import Tensor
1✔
14
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer as sga
1✔
15

16
try:
1✔
17
    from phonopy import Phonopy
1✔
18
    from phonopy.harmonic import dynmat_to_fc as dyntofc
1✔
19
except ImportError:
×
20
    Phonopy = None
×
21

22
__author__ = "Handong Ling"
1✔
23
__copyright__ = "Copyright 2019, The Materials Project"
1✔
24
__version__ = "1.0"
1✔
25
__maintainer__ = "Handong Ling"
1✔
26
__email__ = "hling@lbl.gov"
1✔
27
__status__ = "Development"
1✔
28
__date__ = "Feb, 2019"
1✔
29

30

31
class BornEffectiveCharge:
1✔
32
    """
33
    This class describes the Nx3x3 born effective charge tensor
34
    """
35

36
    def __init__(self, structure: Structure, bec, pointops, tol: float = 1e-3):
1✔
37
        """
38
        Create an BornEffectiveChargeTensor object defined by a
39
        structure, point operations of the structure's atomic sites.
40
        Note that the constructor uses __new__ rather than __init__
41
        according to the standard method of subclassing numpy ndarrays.
42

43
        Args:
44
            input_matrix (Nx3x3 array-like): the Nx3x3 array-like
45
                representing the born effective charge tensor
46
        """
47
        self.structure = structure
1✔
48
        self.bec = bec
1✔
49
        self.pointops = pointops
1✔
50
        self.BEC_operations = None
1✔
51
        if np.sum(self.bec) >= tol:
1✔
52
            warnings.warn("Input born effective charge tensor does not satisfy charge neutrality")
×
53

54
    def get_BEC_operations(self, eigtol=1e-05, opstol=1e-03):
1✔
55
        """
56
        Returns the symmetry operations which maps the tensors
57
        belonging to equivalent sites onto each other in the form
58
        [site index 1, site index 2, [Symmops mapping from site
59
        index 1 to site index 2]]
60

61

62
        Args:
63
            eigtol (float): tolerance for determining if two sites are
64
            related by symmetry
65
            opstol (float): tolerance for determining if a symmetry
66
            operation relates two sites
67

68
        Return:
69
            list of symmetry operations mapping equivalent sites and
70
            the indexes of those sites.
71
        """
72
        bec = self.bec
1✔
73
        struct = self.structure
1✔
74
        ops = sga(struct).get_symmetry_operations(cartesian=True)
1✔
75
        uniq_point_ops = []
1✔
76
        for op in ops:
1✔
77
            uniq_point_ops.append(op)
1✔
78

79
        for ops in self.pointops:
1✔
80
            for op in ops:
1✔
81
                if op not in uniq_point_ops:
1✔
82
                    uniq_point_ops.append(op)
1✔
83

84
        passed = []
1✔
85
        relations = []
1✔
86
        for site, val in enumerate(bec):
1✔
87
            unique = 1
1✔
88
            eig1, vecs1 = np.linalg.eig(val)
1✔
89
            index = np.argsort(eig1)
1✔
90
            new_eig = np.real([eig1[index[0]], eig1[index[1]], eig1[index[2]]])
1✔
91
            for index, p in enumerate(passed):
1✔
92
                if np.allclose(new_eig, p[1], atol=eigtol):
1✔
93
                    relations.append([site, index])
1✔
94
                    unique = 0
1✔
95
                    passed.append([site, p[0], new_eig])
1✔
96
                    break
1✔
97
            if unique == 1:
1✔
98
                relations.append([site, site])
1✔
99
                passed.append([site, new_eig])
1✔
100
        BEC_operations = []
1✔
101
        for atom, r in enumerate(relations):
1✔
102
            BEC_operations.append(r)
1✔
103
            BEC_operations[atom].append([])
1✔
104

105
            for op in uniq_point_ops:
1✔
106
                new = op.transform_tensor(self.bec[relations[atom][1]])
1✔
107

108
                # Check the matrix it references
109
                if np.allclose(new, self.bec[r[0]], atol=opstol):
1✔
110
                    BEC_operations[atom][2].append(op)
1✔
111

112
        self.BEC_operations = BEC_operations
1✔
113
        return BEC_operations
1✔
114

115
    def get_rand_BEC(self, max_charge=1):
1✔
116
        """
117
        Generate a random born effective charge tensor which obeys a structure's
118
        symmetry and the acoustic sum rule
119

120
        Args:
121
            max_charge (float): maximum born effective charge value
122

123
        Return:
124
            np.array Born effective charge tensor
125
        """
126
        struct = self.structure
1✔
127
        symstruc = sga(struct)
1✔
128
        symstruc = symstruc.get_symmetrized_structure()
1✔
129

130
        l = len(struct)
1✔
131
        BEC = np.zeros((l, 3, 3))
1✔
132
        for atom, ops in enumerate(self.BEC_operations):
1✔
133
            if ops[0] == ops[1]:
1✔
134
                temp_tensor = Tensor(np.random.rand(3, 3) - 0.5)
1✔
135
                temp_tensor = sum(temp_tensor.transform(symm_op) for symm_op in self.pointops[atom]) / len(
1✔
136
                    self.pointops[atom]
137
                )
138
                BEC[atom] = temp_tensor
1✔
139
            else:
140
                tempfcm = np.zeros([3, 3])
1✔
141
                for op in ops[2]:
1✔
142
                    tempfcm += op.transform_tensor(BEC[self.BEC_operations[atom][1]])
1✔
143
                BEC[ops[0]] = tempfcm
1✔
144
                if len(ops[2]) != 0:
1✔
145
                    BEC[ops[0]] = BEC[ops[0]] / len(ops[2])
1✔
146

147
        #     Enforce Acoustic Sum
148
        disp_charge = np.einsum("ijk->jk", BEC) / l
1✔
149
        add = np.zeros([l, 3, 3])
1✔
150

151
        for atom, ops in enumerate(self.BEC_operations):
1✔
152
            if ops[0] == ops[1]:
1✔
153
                temp_tensor = Tensor(disp_charge)
1✔
154
                temp_tensor = sum(temp_tensor.transform(symm_op) for symm_op in self.pointops[atom]) / len(
1✔
155
                    self.pointops[atom]
156
                )
157
                add[ops[0]] = temp_tensor
1✔
158
            else:
159
                temp_tensor = np.zeros([3, 3])
1✔
160
                for op in ops[2]:
1✔
161
                    temp_tensor += op.transform_tensor(add[self.BEC_operations[atom][1]])
1✔
162

163
                add[ops[0]] = temp_tensor
1✔
164

165
                if len(ops) != 0:
1✔
166
                    add[ops[0]] = add[ops[0]] / len(ops[2])
1✔
167

168
        BEC = BEC - add
1✔
169

170
        return BEC * max_charge
1✔
171

172

173
class InternalStrainTensor:
1✔
174
    """
175
    This class describes the Nx3x3x3 internal tensor defined by a
176
    structure, point operations of the structure's atomic sites.
177
    """
178

179
    def __init__(self, structure: Structure, ist, pointops, tol: float = 1e-3):
1✔
180
        """
181
        Create an InternalStrainTensor object.
182

183
        Args:
184
            input_matrix (Nx3x3x3 array-like): the Nx3x3x3 array-like
185
                representing the internal strain tensor
186
        """
187
        self.structure = structure
1✔
188
        self.ist = ist
1✔
189
        self.pointops = pointops
1✔
190
        self.IST_operations = None
1✔
191

192
        obj = self.ist
1✔
193
        if not (obj - np.transpose(obj, (0, 1, 3, 2)) < tol).all():
1✔
194
            warnings.warn("Input internal strain tensor does not satisfy standard symmetries")
×
195

196
    def get_IST_operations(self, opstol=1e-03):
1✔
197
        """
198
        Returns the symmetry operations which maps the tensors
199
        belonging to equivalent sites onto each other in the form
200
        [site index 1, site index 2, [Symmops mapping from site
201
        index 1 to site index 2]]
202

203

204
        Args:
205
            opstol (float): tolerance for determining if a symmetry
206
            operation relates two sites
207

208
        Return:
209
            list of symmetry operations mapping equivalent sites and
210
            the indexes of those sites.
211
        """
212
        struct = self.structure
1✔
213
        ops = sga(struct).get_symmetry_operations(cartesian=True)
1✔
214
        uniquepointops = []
1✔
215
        for op in ops:
1✔
216
            uniquepointops.append(op)
1✔
217

218
        for ops in self.pointops:
1✔
219
            for op in ops:
1✔
220
                if op not in uniquepointops:
1✔
221
                    uniquepointops.append(op)
1✔
222

223
        IST_operations = []
1✔
224
        for atom in range(len(self.ist)):  # pylint: disable=C0200
1✔
225
            IST_operations.append([])
1✔
226
            for j in range(0, atom):
1✔
227
                for op in uniquepointops:
1✔
228
                    new = op.transform_tensor(self.ist[j])
1✔
229

230
                    # Check the matrix it references
231
                    if np.allclose(new, self.ist[atom], atol=opstol):
1✔
232
                        IST_operations[atom].append([j, op])
1✔
233

234
        self.IST_operations = IST_operations
1✔
235

236
    def get_rand_IST(self, max_force=1):
1✔
237
        """
238
        Generate a random internal strain tensor which obeys a structure's
239
        symmetry and the acoustic sum rule
240

241
        Args:
242
            max_force(float): maximum born effective charge value
243

244
        Return:
245
            InternalStrainTensor object
246
        """
247
        l = len(self.structure)
1✔
248
        IST = np.zeros((l, 3, 3, 3))
1✔
249
        for atom, ops in enumerate(self.IST_operations):
1✔
250
            temp_tensor = np.zeros([3, 3, 3])
1✔
251
            for op in ops:
1✔
252
                temp_tensor += op[1].transform_tensor(IST[op[0]])
1✔
253

254
            if len(ops) == 0:
1✔
255
                temp_tensor = Tensor(np.random.rand(3, 3, 3) - 0.5)
1✔
256
                for dim in range(3):
1✔
257
                    temp_tensor[dim] = (temp_tensor[dim] + temp_tensor[dim].T) / 2
1✔
258
                temp_tensor = sum(temp_tensor.transform(symm_op) for symm_op in self.pointops[atom]) / len(
1✔
259
                    self.pointops[atom]
260
                )
261
            IST[atom] = temp_tensor
1✔
262
            if len(ops) != 0:
1✔
263
                IST[atom] = IST[atom] / len(ops)
1✔
264

265
        return IST * max_force
1✔
266

267

268
class ForceConstantMatrix:
1✔
269
    """
270
    This class describes the NxNx3x3 force constant matrix defined by a
271
    structure, point operations of the structure's atomic sites, and the
272
    shared symmetry operations between pairs of atomic sites.
273
    """
274

275
    def __init__(self, structure: Structure, fcm, pointops, sharedops, tol: float = 1e-3):
1✔
276
        """
277
        Create an ForceConstantMatrix object.
278

279
        Args:
280
            input_matrix (NxNx3x3 array-like): the NxNx3x3 array-like
281
                representing the force constant matrix
282
        """
283
        self.structure = structure
1✔
284
        self.fcm = fcm
1✔
285
        self.pointops = pointops
1✔
286
        self.sharedops = sharedops
1✔
287
        self.FCM_operations = None
1✔
288

289
    def get_FCM_operations(self, eigtol=1e-05, opstol=1e-05):
1✔
290
        """
291
        Returns the symmetry operations which maps the tensors
292
        belonging to equivalent sites onto each other in the form
293
        [site index 1a, site index 1b, site index 2a, site index 2b,
294
        [Symmops mapping from site index 1a, 1b to site index 2a, 2b]]
295

296

297
        Args:
298
            eigtol (float): tolerance for determining if two sites are
299
            related by symmetry
300
            opstol (float): tolerance for determining if a symmetry
301
            operation relates two sites
302

303
        Return:
304
            list of symmetry operations mapping equivalent sites and
305
            the indexes of those sites.
306
        """
307
        struct = self.structure
1✔
308
        ops = sga(struct).get_symmetry_operations(cartesian=True)
1✔
309
        uniquepointops = []
1✔
310
        for op in ops:
1✔
311
            uniquepointops.append(op)
1✔
312

313
        for ops in self.pointops:
1✔
314
            for op in ops:
1✔
315
                if op not in uniquepointops:
1✔
316
                    uniquepointops.append(op)
1✔
317

318
        passed = []
1✔
319
        relations = []
1✔
320
        for atom1 in range(len(self.fcm)):  # pylint: disable=C0200
1✔
321
            for atom2 in range(atom1, len(self.fcm)):
1✔
322
                unique = 1
1✔
323
                eig1, vecs1 = np.linalg.eig(self.fcm[atom1][atom2])
1✔
324
                index = np.argsort(eig1)
1✔
325
                neweig = np.real([eig1[index[0]], eig1[index[1]], eig1[index[2]]])
1✔
326

327
                for p in passed:
1✔
328
                    if np.allclose(neweig, p[2], atol=eigtol):
1✔
329
                        relations.append([atom1, atom2, p[0], p[1]])
1✔
330
                        unique = 0
1✔
331
                        break
1✔
332
                if unique == 1:
1✔
333
                    relations.append([atom1, atom2, atom2, atom1])
1✔
334
                    passed.append([atom1, atom2, np.real(neweig)])
1✔
335
        FCM_operations = []
1✔
336
        for entry, r in enumerate(relations):
1✔
337
            FCM_operations.append(r)
1✔
338
            FCM_operations[entry].append([])
1✔
339

340
            good = 0
1✔
341
            for op in uniquepointops:
1✔
342
                new = op.transform_tensor(self.fcm[r[2]][r[3]])
1✔
343

344
                if np.allclose(new, self.fcm[r[0]][r[1]], atol=opstol):
1✔
345
                    FCM_operations[entry][4].append(op)
1✔
346
                    good = 1
1✔
347
            if r[0] == r[3] and r[1] == r[2]:
1✔
348
                good = 1
1✔
349
            if r[0] == r[2] and r[1] == r[3]:
1✔
350
                good = 1
1✔
351
            if good == 0:
1✔
352
                FCM_operations[entry] = [r[0], r[1], r[3], r[2]]
×
353
                FCM_operations[entry].append([])
×
354
                for op in uniquepointops:
×
355
                    new = op.transform_tensor(self.fcm[r[2]][r[3]])
×
356
                    if np.allclose(
×
357
                        new.T,
358
                        self.fcm[r[0]][r[1]],
359
                        atol=opstol,
360
                    ):
361
                        FCM_operations[entry][4].append(op)
×
362

363
        self.FCM_operations = FCM_operations
1✔
364
        return FCM_operations
1✔
365

366
    def get_unstable_FCM(self, max_force=1):
1✔
367
        """
368
        Generate an unsymmeterized force constant matrix
369

370
        Args:
371
            max_charge (float): maximum born effective charge value
372

373
        Return:
374
            numpy array representing the force constant matrix
375
        """
376
        struct = self.structure
1✔
377
        operations = self.FCM_operations
1✔
378
        # set max force in reciprocal space
379
        numsites = len(struct.sites)
1✔
380
        D = (1 / max_force) * 2 * (np.ones([numsites * 3, numsites * 3]))
1✔
381
        for op in operations:
1✔
382
            same = 0
1✔
383
            transpose = 0
1✔
384
            if op[0] == op[1] and op[0] == op[2] and op[0] == op[3]:
1✔
385
                same = 1
1✔
386
            if op[0] == op[3] and op[1] == op[2]:
1✔
387
                transpose = 1
1✔
388
            if transpose == 0 and same == 0:
1✔
389
                D[3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3] = np.zeros([3, 3])
1✔
390
                D[3 * op[1] : 3 * op[1] + 3, 3 * op[0] : 3 * op[0] + 3] = np.zeros([3, 3])
1✔
391

392
                for symop in op[4]:
1✔
393
                    tempfcm = D[3 * op[2] : 3 * op[2] + 3, 3 * op[3] : 3 * op[3] + 3]
1✔
394
                    tempfcm = symop.transform_tensor(tempfcm)
1✔
395
                    D[3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3] += tempfcm
1✔
396

397
                if len(op[4]) != 0:
1✔
398
                    D[3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3] = D[
1✔
399
                        3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3
400
                    ] / len(op[4])
401

402
                D[3 * op[1] : 3 * op[1] + 3, 3 * op[0] : 3 * op[0] + 3] = D[
1✔
403
                    3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3
404
                ].T
405
                continue
1✔
406

407
            temp_tensor = Tensor(np.random.rand(3, 3) - 0.5) * max_force
1✔
408

409
            temp_tensor_sum = sum(temp_tensor.transform(symm_op) for symm_op in self.sharedops[op[0]][op[1]])
1✔
410
            temp_tensor_sum = temp_tensor_sum / (len(self.sharedops[op[0]][op[1]]))
1✔
411
            if op[0] != op[1]:
1✔
412
                for pair in range(len(op[4])):
1✔
413
                    temp_tensor2 = temp_tensor_sum.T
1✔
414
                    temp_tensor2 = op[4][pair].transform_tensor(temp_tensor2)
1✔
415
                    temp_tensor_sum = (temp_tensor_sum + temp_tensor2) / 2
1✔
416

417
            else:
418
                temp_tensor_sum = (temp_tensor_sum + temp_tensor_sum.T) / 2
1✔
419

420
            D[3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3] = temp_tensor_sum
1✔
421
            D[3 * op[1] : 3 * op[1] + 3, 3 * op[0] : 3 * op[0] + 3] = temp_tensor_sum.T
1✔
422

423
        return D
1✔
424

425
    def get_symmetrized_FCM(self, unsymmetrized_fcm, max_force=1):
1✔
426
        """
427
        Generate a symmeterized force constant matrix from an unsymmeterized matrix
428

429
        Args:
430
            unsymmetrized_fcm (numpy array): unsymmeterized force constant matrix
431
            max_charge (float): maximum born effective charge value
432

433
        Return:
434
            3Nx3N numpy array representing the force constant matrix
435
        """
436
        operations = self.FCM_operations
1✔
437
        D = unsymmetrized_fcm
1✔
438
        for op in operations:
1✔
439
            same = 0
1✔
440
            transpose = 0
1✔
441
            if op[0] == op[1] and op[0] == operations[2] and op[0] == op[3]:
1✔
442
                same = 1
×
443
            if op[0] == op[3] and op[1] == op[2]:
1✔
444
                transpose = 1
1✔
445
            if transpose == 0 and same == 0:
1✔
446
                D[3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3] = np.zeros([3, 3])
1✔
447

448
                for symop in op[4]:
1✔
449
                    tempfcm = D[3 * op[2] : 3 * op[2] + 3, 3 * op[3] : 3 * op[3] + 3]
1✔
450
                    tempfcm = symop.transform_tensor(tempfcm)
1✔
451

452
                    D[3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3] += tempfcm
1✔
453

454
                if len(op[4]) != 0:
1✔
455
                    D[3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3] = D[
1✔
456
                        3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3
457
                    ] / len(op[4])
458
                D[3 * op[1] : 3 * op[1] + 3, 3 * op[0] : 3 * op[0] + 3] = D[
1✔
459
                    3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3
460
                ].T
461
                continue
1✔
462

463
            temp_tensor = Tensor(D[3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3])
1✔
464
            temp_tensor_sum = sum(temp_tensor.transform(symm_op) for symm_op in self.sharedops[op[0]][op[1]])
1✔
465
            if len(self.sharedops[op[0]][op[1]]) != 0:
1✔
466
                temp_tensor_sum = temp_tensor_sum / (len(self.sharedops[op[0]][op[1]]))
1✔
467

468
            # Apply the proper transformation if there is an equivalent already
469
            if op[0] != op[1]:
1✔
470
                for pair in range(len(op[4])):
1✔
471
                    temp_tensor2 = temp_tensor_sum.T
1✔
472
                    temp_tensor2 = op[4][pair].transform_tensor(temp_tensor2)
1✔
473
                    temp_tensor_sum = (temp_tensor_sum + temp_tensor2) / 2
1✔
474

475
            else:
476
                temp_tensor_sum = (temp_tensor_sum + temp_tensor_sum.T) / 2
1✔
477

478
            D[3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3] = temp_tensor_sum
1✔
479
            D[3 * op[1] : 3 * op[1] + 3, 3 * op[0] : 3 * op[0] + 3] = temp_tensor_sum.T
1✔
480

481
        return D
1✔
482

483
    def get_stable_FCM(self, fcm, fcmasum=10):
1✔
484
        """
485
        Generate a symmeterized force constant matrix that obeys the objects symmetry
486
        constraints, has no unstable modes and also obeys the acoustic sum rule through an
487
        iterative procedure
488

489
        Args:
490
            fcm (numpy array): unsymmeterized force constant matrix
491
            fcmasum (int): number of iterations to attempt to obey the acoustic sum
492
                rule
493

494
        Return:
495
            3Nx3N numpy array representing the force constant matrix
496
        """
497
        check = 0
1✔
498
        count = 0
1✔
499
        while check == 0:
1✔
500
            # if resymmetrizing brings back unstable modes 20 times, the method breaks
501
            if count > 20:
1✔
502
                check = 1
×
503
                break
×
504

505
            eigs, vecs = np.linalg.eig(fcm)
1✔
506

507
            maxeig = np.max(-1 * eigs)
1✔
508
            eigsort = np.argsort(np.abs(eigs))
1✔
509
            for i in range(3, len(eigs)):
1✔
510
                if eigs[eigsort[i]] > 1e-06:
1✔
511
                    eigs[eigsort[i]] = -1 * maxeig * np.random.rand()
1✔
512
            diag = np.real(np.eye(len(fcm)) * eigs)
1✔
513

514
            fcm = np.real(np.matmul(np.matmul(vecs, diag), vecs.T))
1✔
515
            fcm = self.get_symmetrized_FCM(fcm)
1✔
516
            fcm = self.get_asum_FCM(fcm)
1✔
517
            eigs, vecs = np.linalg.eig(fcm)
1✔
518
            unstable_modes = 0
1✔
519
            eigsort = np.argsort(np.abs(eigs))
1✔
520
            for i in range(3, len(eigs)):
1✔
521
                if eigs[eigsort[i]] > 1e-06:
1✔
522
                    unstable_modes = 1
1✔
523
            if unstable_modes == 1:
1✔
524
                count = count + 1
1✔
525
                continue
1✔
526
            check = 1
1✔
527

528
        return fcm
1✔
529

530
    # acoustic sum
531

532
    def get_asum_FCM(self, fcm: np.ndarray, numiter: int = 15):
1✔
533
        """
534
        Generate a symmeterized force constant matrix that obeys the objects symmetry
535
        constraints and obeys the acoustic sum rule through an iterative procedure
536

537
        Args:
538
            fcm (numpy array): 3Nx3N unsymmeterized force constant matrix
539
            numiter (int): number of iterations to attempt to obey the acoustic sum
540
                rule
541

542
        Return:
543
            numpy array representing the force constant matrix
544
        """
545
        # set max force in reciprocal space
546
        operations = self.FCM_operations
1✔
547
        assert operations is not None, "No symmetry operations found"
1✔
548

549
        numsites = len(self.structure)
1✔
550

551
        D = np.ones([numsites * 3, numsites * 3])
1✔
552
        for _ in range(numiter):
1✔
553
            X = np.real(fcm)
1✔
554

555
            # symmetry operations
556
            pastrow = 0
1✔
557
            total = np.zeros([3, 3])
1✔
558
            for col in range(numsites):
1✔
559
                total = total + X[0:3, col * 3 : col * 3 + 3]
1✔
560

561
            total = total / (numsites)
1✔
562
            for op in operations:
1✔
563
                same = 0
1✔
564
                transpose = 0
1✔
565
                if op[0] == op[1] and op[0] == op[2] and op[0] == op[3]:
1✔
566
                    same = 1
1✔
567
                if op[0] == op[3] and op[1] == op[2]:
1✔
568
                    transpose = 1
1✔
569
                if transpose == 0 and same == 0:
1✔
570
                    D[3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3] = np.zeros([3, 3])
1✔
571

572
                    for symop in op[4]:
1✔
573
                        tempfcm = D[3 * op[2] : 3 * op[2] + 3, 3 * op[3] : 3 * op[3] + 3]
1✔
574
                        tempfcm = symop.transform_tensor(tempfcm)
1✔
575

576
                        D[3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3] += tempfcm
1✔
577

578
                    if len(op[4]) != 0:
1✔
579
                        D[3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3] = D[
1✔
580
                            3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3
581
                        ] / len(op[4])
582
                    D[3 * op[1] : 3 * op[1] + 3, 3 * op[0] : 3 * op[0] + 3] = D[
1✔
583
                        3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3
584
                    ].T
585
                    continue
1✔
586
                # Get the difference in the sum up to this point
587
                currrow = op[0]
1✔
588
                if currrow != pastrow:
1✔
589
                    total = np.zeros([3, 3])
1✔
590
                    for col in range(numsites):
1✔
591
                        total = total + X[currrow * 3 : currrow * 3 + 3, col * 3 : col * 3 + 3]
1✔
592
                    for col in range(currrow):
1✔
593
                        total = total - D[currrow * 3 : currrow * 3 + 3, col * 3 : col * 3 + 3]
1✔
594
                    total = total / (numsites - currrow)
1✔
595
                pastrow = currrow
1✔
596

597
                # Apply the point symmetry operations of the site
598
                temp_tensor = Tensor(total)
1✔
599
                temp_tensor_sum = sum(temp_tensor.transform(symm_op) for symm_op in self.sharedops[op[0]][op[1]])
1✔
600

601
                if len(self.sharedops[op[0]][op[1]]) != 0:
1✔
602
                    temp_tensor_sum = temp_tensor_sum / (len(self.sharedops[op[0]][op[1]]))
1✔
603

604
                # Apply the proper transformation if there is an equivalent already
605
                if op[0] != op[1]:
1✔
606
                    for pair in range(len(op[4])):
1✔
607
                        temp_tensor2 = temp_tensor_sum.T
1✔
608
                        temp_tensor2 = op[4][pair].transform_tensor(temp_tensor2)
1✔
609
                        temp_tensor_sum = (temp_tensor_sum + temp_tensor2) / 2
1✔
610

611
                else:
612
                    temp_tensor_sum = (temp_tensor_sum + temp_tensor_sum.T) / 2
1✔
613

614
                D[3 * op[0] : 3 * op[0] + 3, 3 * op[1] : 3 * op[1] + 3] = temp_tensor_sum
1✔
615
                D[3 * op[1] : 3 * op[1] + 3, 3 * op[0] : 3 * op[0] + 3] = temp_tensor_sum.T
1✔
616
            fcm = fcm - D
1✔
617

618
        return fcm
1✔
619

620
    @requires(Phonopy, "phonopy not installed!")
1✔
621
    def get_rand_FCM(self, asum=15, force=10):
1✔
622
        """
623
        Generate a symmeterized force constant matrix from an unsymmeterized matrix
624
        that has no unstable modes and also obeys the acoustic sum rule through an
625
        iterative procedure
626

627
        Args:
628
            force (float): maximum force constant
629
            asum (int): number of iterations to attempt to obey the acoustic sum
630
                rule
631

632
        Return:
633
            NxNx3x3 np.array representing the force constant matrix
634
        """
635
        from pymatgen.io.phonopy import get_phonopy_structure
1✔
636

637
        numsites = len(self.structure.sites)
1✔
638
        structure = get_phonopy_structure(self.structure)
1✔
639
        pnstruc = Phonopy(structure, np.eye(3), np.eye(3))
1✔
640

641
        dyn = self.get_unstable_FCM(force)
1✔
642
        dyn = self.get_stable_FCM(dyn)
1✔
643

644
        dyn = np.reshape(dyn, (numsites, 3, numsites, 3)).swapaxes(1, 2)
1✔
645

646
        dynmass = np.zeros([len(self.structure), len(self.structure), 3, 3])
1✔
647
        masses = []
1✔
648
        for j in range(numsites):
1✔
649
            masses.append(self.structure.sites[j].specie.atomic_mass)
1✔
650
        dynmass = np.zeros([numsites, numsites, 3, 3])
1✔
651
        for m in range(numsites):
1✔
652
            for n in range(numsites):
1✔
653
                dynmass[m][n] = dyn[m][n] * np.sqrt(masses[m]) * np.sqrt(masses[n])
1✔
654

655
        supercell = pnstruc.get_supercell()
1✔
656
        primitive = pnstruc.get_primitive()
1✔
657

658
        converter = dyntofc.DynmatToForceConstants(primitive, supercell)
1✔
659

660
        dyn = np.reshape(np.swapaxes(dynmass, 1, 2), (numsites * 3, numsites * 3))
1✔
661

662
        converter.set_dynamical_matrices(dynmat=[dyn])
1✔
663

664
        converter.run()
1✔
665
        fc = converter.get_force_constants()
1✔
666

667
        return fc
1✔
668

669

670
def get_piezo(BEC, IST, FCM, rcond=0.0001):
1✔
671
    """
672
    Generate a random piezoelectric tensor based on a structure and corresponding
673
    symmetry
674

675
    Args:
676
        BEC (numpy array): Nx3x3 array representing the born effective charge tensor
677
        IST (numpy array): Nx3x3x3 array representing the internal strain tensor
678
        FCM (numpy array): NxNx3x3 array representing the born effective charge tensor
679
        rcondy (float): condition for excluding eigenvalues in the pseudoinverse
680

681
    Return:
682
        3x3x3 calculated Piezo tensor
683
    """
684

685
    numsites = len(BEC)
1✔
686
    temp_fcm = np.reshape(np.swapaxes(FCM, 1, 2), (numsites * 3, numsites * 3))
1✔
687

688
    eigs, vecs = np.linalg.eig(temp_fcm)
1✔
689
    K = np.linalg.pinv(
1✔
690
        -temp_fcm,
691
        rcond=np.abs(eigs[np.argsort(np.abs(eigs))[2]]) / np.abs(eigs[np.argsort(np.abs(eigs))[-1]]) + rcond,
692
    )
693

694
    K = np.reshape(K, (numsites, 3, numsites, 3)).swapaxes(1, 2)
1✔
695
    return np.einsum("ikl,ijlm,jmno->kno", BEC, K, IST) * 16.0216559424
1✔
696

697

698
@requires(Phonopy, "phonopy not installed!")
1✔
699
def rand_piezo(struct, pointops, sharedops, BEC, IST, FCM, anumiter=10):
1✔
700
    """
701
    Generate a random piezoelectric tensor based on a structure and corresponding
702
    symmetry
703

704
    Args:
705
        struct (pymatgen structure): structure whose symmetry operations the piezo tensor must obey
706
        pointops: list of point operations obeyed by a single atomic site
707
        sharedops: list of point operations shared by a pair of atomic sites
708
        BEC (numpy array): Nx3x3 array representing the born effective charge tensor
709
        IST (numpy array): Nx3x3x3 array representing the internal strain tensor
710
        FCM (numpy array): NxNx3x3 array representing the born effective charge tensor
711
        anumiter (int): number of iterations for acoustic sum rule convergence
712
    Return:
713
        list in the form of [Nx3x3 random born effective charge tenosr,
714
        Nx3x3x3 random internal strain tensor, NxNx3x3 random force constant matrix, 3x3x3 piezo tensor]
715
    """
716
    bec = BornEffectiveCharge(struct, BEC, pointops)
1✔
717
    bec.get_BEC_operations()
1✔
718
    rand_BEC = bec.get_rand_BEC()
1✔
719

720
    ist = InternalStrainTensor(struct, IST, pointops)
1✔
721
    ist.get_IST_operations()
1✔
722
    rand_IST = ist.get_rand_IST()
1✔
723

724
    fcm = ForceConstantMatrix(struct, FCM, pointops, sharedops)
1✔
725
    fcm.get_FCM_operations()
1✔
726
    rand_FCM = fcm.get_rand_FCM()
1✔
727

728
    P = get_piezo(rand_BEC, rand_IST, rand_FCM) * 16.0216559424 / struct.volume
1✔
729

730
    return (rand_BEC, rand_IST, rand_FCM, P)
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