• 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

97.88
/pymatgen/analysis/elasticity/elastic.py
1
# Copyright (c) Pymatgen Development Team.
2
# Distributed under the terms of the MIT License.
3

4
"""
1✔
5
This module provides a class used to describe the elastic tensor,
6
including methods used to fit the elastic tensor from linear response
7
stress-strain data
8
"""
9

10
from __future__ import annotations
1✔
11

12
import itertools
1✔
13
import warnings
1✔
14

15
import numpy as np
1✔
16
import sympy as sp
1✔
17
from scipy.integrate import quad
1✔
18
from scipy.optimize import root
1✔
19
from scipy.special import factorial
1✔
20

21
from pymatgen.analysis.elasticity.strain import Strain
1✔
22
from pymatgen.analysis.elasticity.stress import Stress
1✔
23
from pymatgen.core.structure import Structure
1✔
24
from pymatgen.core.tensors import (
1✔
25
    DEFAULT_QUAD,
26
    SquareTensor,
27
    Tensor,
28
    TensorCollection,
29
    get_uvec,
30
)
31
from pymatgen.core.units import Unit
1✔
32

33
__author__ = "Joseph Montoya"
1✔
34
__copyright__ = "Copyright 2012, The Materials Project"
1✔
35
__credits__ = "Maarten de Jong, Ian Winter, Shyam Dwaraknath, Mark Asta, Anubhav Jain"
1✔
36
__version__ = "1.0"
1✔
37
__maintainer__ = "Joseph Montoya"
1✔
38
__email__ = "montoyjh@lbl.gov"
1✔
39
__status__ = "Production"
1✔
40
__date__ = "July 24, 2018"
1✔
41

42

43
class NthOrderElasticTensor(Tensor):
1✔
44
    """
45
    An object representing an nth-order tensor expansion
46
    of the stress-strain constitutive equations
47
    """
48

49
    GPa_to_eV_A3 = Unit("GPa").get_conversion_factor(Unit("eV ang^-3"))
1✔
50
    symbol = "C"
1✔
51

52
    def __new__(cls, input_array, check_rank=None, tol: float = 1e-4):
1✔
53
        """
54
        Args:
55
            input_array ():
56
            check_rank ():
57
            tol ():
58
        """
59
        obj = super().__new__(cls, input_array, check_rank=check_rank)
1✔
60
        if obj.rank % 2 != 0:
1✔
61
            raise ValueError("ElasticTensor must have even rank")
1✔
62
        if not obj.is_voigt_symmetric(tol):
1✔
63
            warnings.warn("Input elastic tensor does not satisfy standard Voigt symmetries")
1✔
64
        return obj.view(cls)
1✔
65

66
    @property
1✔
67
    def order(self):
1✔
68
        """
69
        Order of the elastic tensor
70
        """
71
        return self.rank // 2
1✔
72

73
    def calculate_stress(self, strain):
1✔
74
        """
75
        Calculate's a given elastic tensor's contribution to the
76
        stress using Einstein summation
77

78
        Args:
79
            strain (3x3 array-like): matrix corresponding to strain
80
        """
81
        strain = np.array(strain)
1✔
82
        if strain.shape == (6,):
1✔
83
            strain = Strain.from_voigt(strain)
1✔
84
        assert strain.shape == (3, 3), "Strain must be 3x3 or voigt-notation"
1✔
85
        stress_matrix = self.einsum_sequence([strain] * (self.order - 1)) / factorial(self.order - 1)
1✔
86
        return Stress(stress_matrix)
1✔
87

88
    def energy_density(self, strain, convert_GPa_to_eV=True):
1✔
89
        """
90
        Calculates the elastic energy density due to a strain
91
        """
92
        e_density = np.sum(self.calculate_stress(strain) * strain) / self.order
1✔
93
        if convert_GPa_to_eV:
1✔
94
            e_density *= self.GPa_to_eV_A3  # Conversion factor for GPa to eV/A^3
1✔
95
        return e_density
1✔
96

97
    @classmethod
1✔
98
    def from_diff_fit(cls, strains, stresses, eq_stress=None, order=2, tol: float = 1e-10):
1✔
99
        """
100
        Takes a list of strains and stresses, and returns a list of coefficients for a
101
        polynomial fit of the given order.
102

103
        Args:
104
            strains: a list of strain values
105
            stresses: the stress values
106
            eq_stress: The stress at which the material is assumed to be elastic.
107
            order: The order of the polynomial to fit. Defaults to 2
108
            tol (float): tolerance for the fit.
109

110
        Returns:
111
            NthOrderElasticTensor: the fitted elastic tensor
112
        """
113
        return cls(diff_fit(strains, stresses, eq_stress, order, tol)[order - 2])
1✔
114

115

116
def raise_error_if_unphysical(f):
1✔
117
    """
118
    Wrapper for functions or properties that should raise an error
119
    if tensor is unphysical.
120
    """
121

122
    def wrapper(self, *args, **kwargs):
1✔
123
        """
124
        Args:
125
            self ():
126
            *args ():
127
            **kwargs ():
128

129
        Returns:
130
        """
131
        if self.k_vrh < 0 or self.g_vrh < 0:
1✔
132
            raise ValueError("Bulk or shear modulus is negative, property cannot be determined")
1✔
133
        return f(self, *args, **kwargs)
1✔
134

135
    return wrapper
1✔
136

137

138
class ElasticTensor(NthOrderElasticTensor):
1✔
139
    """
140
    This class extends Tensor to describe the 3x3x3x3
141
    second-order elastic tensor, C_{ijkl}, with various
142
    methods for estimating other properties derived from
143
    the second order elastic tensor
144
    """
145

146
    def __new__(cls, input_array, tol: float = 1e-4):
1✔
147
        """
148
        Create an ElasticTensor object. The constructor throws an error if
149
        the shape of the input_matrix argument is not 3x3x3x3, i. e. in true
150
        tensor notation. Issues a warning if the input_matrix argument does
151
        not satisfy standard symmetries. Note that the constructor uses
152
        __new__ rather than __init__ according to the standard method of
153
        subclassing numpy ndarrays.
154

155
        Args:
156
            input_array (3x3x3x3 array-like): the 3x3x3x3 array-like
157
                representing the elastic tensor
158

159
            tol (float): tolerance for initial symmetry test of tensor
160
        """
161
        obj = super().__new__(cls, input_array, check_rank=4, tol=tol)
1✔
162
        return obj.view(cls)
1✔
163

164
    @property
1✔
165
    def compliance_tensor(self):
1✔
166
        """
167
        Returns the Voigt-notation compliance tensor,
168
        which is the matrix inverse of the
169
        Voigt-notation elastic tensor
170
        """
171
        s_voigt = np.linalg.inv(self.voigt)
1✔
172
        return ComplianceTensor.from_voigt(s_voigt)
1✔
173

174
    @property
1✔
175
    def k_voigt(self):
1✔
176
        """
177
        Returns the K_v bulk modulus
178
        """
179
        return self.voigt[:3, :3].mean()
1✔
180

181
    @property
1✔
182
    def g_voigt(self):
1✔
183
        """
184
        Returns the G_v shear modulus
185
        """
186
        return (
1✔
187
            2.0 * self.voigt[:3, :3].trace() - np.triu(self.voigt[:3, :3]).sum() + 3 * self.voigt[3:, 3:].trace()
188
        ) / 15.0
189

190
    @property
1✔
191
    def k_reuss(self):
1✔
192
        """
193
        Returns the K_r bulk modulus
194
        """
195
        return 1.0 / self.compliance_tensor.voigt[:3, :3].sum()
1✔
196

197
    @property
1✔
198
    def g_reuss(self):
1✔
199
        """
200
        Returns the G_r shear modulus
201
        """
202
        return 15.0 / (
1✔
203
            8.0 * self.compliance_tensor.voigt[:3, :3].trace()
204
            - 4.0 * np.triu(self.compliance_tensor.voigt[:3, :3]).sum()
205
            + 3.0 * self.compliance_tensor.voigt[3:, 3:].trace()
206
        )
207

208
    @property
1✔
209
    def k_vrh(self):
1✔
210
        """
211
        Returns the K_vrh (Voigt-Reuss-Hill) average bulk modulus
212
        """
213
        return 0.5 * (self.k_voigt + self.k_reuss)
1✔
214

215
    @property
1✔
216
    def g_vrh(self):
1✔
217
        """
218
        Returns the G_vrh (Voigt-Reuss-Hill) average shear modulus
219
        """
220
        return 0.5 * (self.g_voigt + self.g_reuss)
1✔
221

222
    @property
1✔
223
    def y_mod(self):
1✔
224
        """
225
        Calculates Young's modulus (in SI units) using the
226
        Voigt-Reuss-Hill averages of bulk and shear moduli
227
        """
228
        return 9.0e9 * self.k_vrh * self.g_vrh / (3.0 * self.k_vrh + self.g_vrh)
1✔
229

230
    def directional_poisson_ratio(self, n, m, tol: float = 1e-8):
1✔
231
        """
232
        Calculates the poisson ratio for a specific direction
233
        relative to a second, orthogonal direction
234

235
        Args:
236
            n (3-d vector): principal direction
237
            m (3-d vector): secondary direction orthogonal to n
238
            tol (float): tolerance for testing of orthogonality
239
        """
240
        n, m = get_uvec(n), get_uvec(m)
1✔
241
        if not np.abs(np.dot(n, m)) < tol:
1✔
242
            raise ValueError("n and m must be orthogonal")
×
243
        v = self.compliance_tensor.einsum_sequence([n] * 2 + [m] * 2)
1✔
244
        v *= -1 / self.compliance_tensor.einsum_sequence([n] * 4)
1✔
245
        return v
1✔
246

247
    def directional_elastic_mod(self, n):
1✔
248
        """
249
        Calculates directional elastic modulus for a specific vector
250
        """
251
        n = get_uvec(n)
1✔
252
        return self.einsum_sequence([n] * 4)
1✔
253

254
    @raise_error_if_unphysical
1✔
255
    def trans_v(self, structure):
1✔
256
        """
257
        Calculates transverse sound velocity (in SI units) using the
258
        Voigt-Reuss-Hill average bulk modulus
259

260
        Args:
261
            structure: pymatgen structure object
262

263
        Returns: transverse sound velocity (in SI units)
264
        """
265
        nsites = structure.num_sites
1✔
266
        volume = structure.volume
1✔
267
        natoms = structure.composition.num_atoms
1✔
268
        weight = float(structure.composition.weight)
1✔
269
        mass_density = 1.6605e3 * nsites * weight / (natoms * volume)
1✔
270
        if self.g_vrh < 0:
1✔
271
            raise ValueError("k_vrh or g_vrh is negative, sound velocity is undefined")
×
272
        return (1e9 * self.g_vrh / mass_density) ** 0.5
1✔
273

274
    @raise_error_if_unphysical
1✔
275
    def long_v(self, structure):
1✔
276
        """
277
        Calculates longitudinal sound velocity (in SI units)
278
        using the Voigt-Reuss-Hill average bulk modulus
279

280
        Args:
281
            structure: pymatgen structure object
282

283
        Returns: longitudinal sound velocity (in SI units)
284
        """
285
        nsites = structure.num_sites
1✔
286
        volume = structure.volume
1✔
287
        natoms = structure.composition.num_atoms
1✔
288
        weight = float(structure.composition.weight)
1✔
289
        mass_density = 1.6605e3 * nsites * weight / (natoms * volume)
1✔
290
        if self.g_vrh < 0:
1✔
291
            raise ValueError("k_vrh or g_vrh is negative, sound velocity is undefined")
×
292
        return (1e9 * (self.k_vrh + 4.0 / 3.0 * self.g_vrh) / mass_density) ** 0.5
1✔
293

294
    @raise_error_if_unphysical
1✔
295
    def snyder_ac(self, structure):
1✔
296
        """
297
        Calculates Snyder's acoustic sound velocity (in SI units)
298

299
        Args:
300
            structure: pymatgen structure object
301

302
        Returns: Snyder's acoustic sound velocity (in SI units)
303
        """
304
        nsites = structure.num_sites
1✔
305
        volume = structure.volume
1✔
306
        natoms = structure.composition.num_atoms
1✔
307
        num_density = 1e30 * nsites / volume
1✔
308
        tot_mass = sum(e.atomic_mass for e in structure.species)
1✔
309
        avg_mass = 1.6605e-27 * tot_mass / natoms
1✔
310
        return (
1✔
311
            0.38483
312
            * avg_mass
313
            * ((self.long_v(structure) + 2.0 * self.trans_v(structure)) / 3.0) ** 3.0
314
            / (300.0 * num_density ** (-2.0 / 3.0) * nsites ** (1.0 / 3.0))
315
        )
316

317
    @raise_error_if_unphysical
1✔
318
    def snyder_opt(self, structure):
1✔
319
        """
320
        Calculates Snyder's optical sound velocity (in SI units)
321

322
        Args:
323
            structure: pymatgen structure object
324

325
        Returns: Snyder's optical sound velocity (in SI units)
326
        """
327
        nsites = structure.num_sites
1✔
328
        volume = structure.volume
1✔
329
        num_density = 1e30 * nsites / volume
1✔
330
        return (
1✔
331
            1.66914e-23
332
            * (self.long_v(structure) + 2.0 * self.trans_v(structure))
333
            / 3.0
334
            / num_density ** (-2.0 / 3.0)
335
            * (1 - nsites ** (-1.0 / 3.0))
336
        )
337

338
    @raise_error_if_unphysical
1✔
339
    def snyder_total(self, structure):
1✔
340
        """
341
        Calculates Snyder's total sound velocity (in SI units)
342

343
        Args:
344
            structure: pymatgen structure object
345

346
        Returns: Snyder's total sound velocity (in SI units)
347
        """
348
        return self.snyder_ac(structure) + self.snyder_opt(structure)
1✔
349

350
    @raise_error_if_unphysical
1✔
351
    def clarke_thermalcond(self, structure):
1✔
352
        """
353
        Calculates Clarke's thermal conductivity (in SI units)
354

355
        Args:
356
            structure: pymatgen structure object
357

358
        Returns: Clarke's thermal conductivity (in SI units)
359
        """
360
        nsites = structure.num_sites
1✔
361
        volume = structure.volume
1✔
362
        tot_mass = sum(e.atomic_mass for e in structure.species)
1✔
363
        natoms = structure.composition.num_atoms
1✔
364
        weight = float(structure.composition.weight)
1✔
365
        avg_mass = 1.6605e-27 * tot_mass / natoms
1✔
366
        mass_density = 1.6605e3 * nsites * weight / (natoms * volume)
1✔
367
        return 0.87 * 1.3806e-23 * avg_mass ** (-2.0 / 3.0) * mass_density ** (1.0 / 6.0) * self.y_mod**0.5
1✔
368

369
    @raise_error_if_unphysical
1✔
370
    def cahill_thermalcond(self, structure):
1✔
371
        """
372
        Calculates Cahill's thermal conductivity (in SI units)
373

374
        Args:
375
            structure: pymatgen structure object
376

377
        Returns: Cahill's thermal conductivity (in SI units)
378
        """
379
        nsites = structure.num_sites
1✔
380
        volume = structure.volume
1✔
381
        num_density = 1e30 * nsites / volume
1✔
382
        return 1.3806e-23 / 2.48 * num_density ** (2.0 / 3.0) * (self.long_v(structure) + 2 * self.trans_v(structure))
1✔
383

384
    @raise_error_if_unphysical
1✔
385
    def debye_temperature(self, structure):
1✔
386
        """
387
        Estimates the debye temperature from longitudinal and
388
        transverse sound velocities
389

390
        Args:
391
            structure: pymatgen structure object
392

393
        Returns: debye temperature (in SI units)
394
        """
395
        v0 = structure.volume * 1e-30 / structure.num_sites
1✔
396
        vl, vt = self.long_v(structure), self.trans_v(structure)
1✔
397
        vm = 3 ** (1.0 / 3.0) * (1 / vl**3 + 2 / vt**3) ** (-1.0 / 3.0)
1✔
398
        td = 1.05457e-34 / 1.38065e-23 * vm * (6 * np.pi**2 / v0) ** (1.0 / 3.0)
1✔
399
        return td
1✔
400

401
    @property
1✔
402
    def universal_anisotropy(self):
1✔
403
        """
404
        Returns the universal anisotropy value
405
        """
406
        return 5.0 * self.g_voigt / self.g_reuss + self.k_voigt / self.k_reuss - 6.0
1✔
407

408
    @property
1✔
409
    def homogeneous_poisson(self):
1✔
410
        """
411
        Returns the homogeneous poisson ratio
412
        """
413
        return (1.0 - 2.0 / 3.0 * self.g_vrh / self.k_vrh) / (2.0 + 2.0 / 3.0 * self.g_vrh / self.k_vrh)
1✔
414

415
    def green_kristoffel(self, u):
1✔
416
        """
417
        Returns the Green-Kristoffel tensor for a second-order tensor
418
        """
419
        return self.einsum_sequence([u, u], "ijkl,i,l")
1✔
420

421
    @property
1✔
422
    def property_dict(self):
1✔
423
        """
424
        Returns a dictionary of properties derived from the elastic tensor
425
        """
426
        props = [
1✔
427
            "k_voigt",
428
            "k_reuss",
429
            "k_vrh",
430
            "g_voigt",
431
            "g_reuss",
432
            "g_vrh",
433
            "universal_anisotropy",
434
            "homogeneous_poisson",
435
            "y_mod",
436
        ]
437
        return {prop: getattr(self, prop) for prop in props}
1✔
438

439
    def get_structure_property_dict(
1✔
440
        self, structure: Structure, include_base_props: bool = True, ignore_errors: bool = False
441
    ) -> dict[str, float | Structure | None]:
442
        """
443
        Returns a dictionary of properties derived from the elastic tensor
444
        and an associated structure
445

446
        Args:
447
            structure (Structure): structure object for which to calculate
448
                associated properties
449
            include_base_props (bool): whether to include base properties,
450
                like k_vrh, etc.
451
            ignore_errors (bool): if set to true, will set problem properties
452
                that depend on a physical tensor to None, defaults to False
453
        """
454
        s_props = [
1✔
455
            "trans_v",
456
            "long_v",
457
            "snyder_ac",
458
            "snyder_opt",
459
            "snyder_total",
460
            "clarke_thermalcond",
461
            "cahill_thermalcond",
462
            "debye_temperature",
463
        ]
464
        sp_dict: dict[str, float | Structure | None]
465
        if ignore_errors and (self.k_vrh < 0 or self.g_vrh < 0):
1✔
466
            sp_dict = {prop: None for prop in s_props}
1✔
467
        else:
468
            sp_dict = {prop: getattr(self, prop)(structure) for prop in s_props}
1✔
469
        sp_dict["structure"] = structure
1✔
470
        if include_base_props:
1✔
471
            sp_dict.update(self.property_dict)
1✔
472
        return sp_dict
1✔
473

474
    @classmethod
1✔
475
    def from_pseudoinverse(cls, strains, stresses):
1✔
476
        """
477
        Class method to fit an elastic tensor from stress/strain
478
        data. Method uses Moore-Penrose pseudoinverse to invert
479
        the s = C*e equation with elastic tensor, stress, and
480
        strain in voigt notation
481

482
        Args:
483
            stresses (Nx3x3 array-like): list or array of stresses
484
            strains (Nx3x3 array-like): list or array of strains
485
        """
486
        # convert the stress/strain to Nx6 arrays of voigt-notation
487
        warnings.warn(
1✔
488
            "Pseudoinverse fitting of Strain/Stress lists may yield "
489
            "questionable results from vasp data, use with caution."
490
        )
491
        stresses = np.array([Stress(stress).voigt for stress in stresses])
1✔
492
        with warnings.catch_warnings(record=True):
1✔
493
            strains = np.array([Strain(strain).voigt for strain in strains])
1✔
494

495
        voigt_fit = np.transpose(np.dot(np.linalg.pinv(strains), stresses))
1✔
496
        return cls.from_voigt(voigt_fit)
1✔
497

498
    @classmethod
1✔
499
    def from_independent_strains(cls, strains, stresses, eq_stress=None, vasp=False, tol: float = 1e-10):
1✔
500
        """
501
        Constructs the elastic tensor least-squares fit of independent strains
502
        Args:
503
            strains (list of Strains): list of strain objects to fit
504
            stresses (list of Stresses): list of stress objects to use in fit
505
                corresponding to the list of strains
506
            eq_stress (Stress): equilibrium stress to use in fitting
507
            vasp (bool): flag for whether the stress tensor should be
508
                converted based on vasp units/convention for stress
509
            tol (float): tolerance for removing near-zero elements of the
510
                resulting tensor
511
        """
512
        strain_states = [tuple(ss) for ss in np.eye(6)]
1✔
513
        ss_dict = get_strain_state_dict(strains, stresses, eq_stress=eq_stress)
1✔
514
        if not set(strain_states) <= set(ss_dict):
1✔
515
            raise ValueError(f"Missing independent strain states: {set(strain_states) - set(ss_dict)}")
×
516
        if len(set(ss_dict) - set(strain_states)) > 0:
1✔
517
            warnings.warn("Extra strain states in strain-stress pairs are neglected in independent strain fitting")
1✔
518
        c_ij = np.zeros((6, 6))
1✔
519
        for i in range(6):
1✔
520
            istrains = ss_dict[strain_states[i]]["strains"]
1✔
521
            istresses = ss_dict[strain_states[i]]["stresses"]
1✔
522
            for j in range(6):
1✔
523
                c_ij[i, j] = np.polyfit(istrains[:, i], istresses[:, j], 1)[0]
1✔
524
        if vasp:
1✔
525
            c_ij *= -0.1  # Convert units/sign convention of vasp stress tensor
×
526
        c = cls.from_voigt(c_ij)
1✔
527
        c = c.zeroed(tol)
1✔
528
        return c
1✔
529

530

531
class ComplianceTensor(Tensor):
1✔
532
    """
533
    This class represents the compliance tensor, and exists
534
    primarily to keep the voigt-conversion scheme consistent
535
    since the compliance tensor has a unique vscale
536
    """
537

538
    def __new__(cls, s_array):
1✔
539
        """
540
        Args:
541
            s_array ():
542
        """
543
        vscale = np.ones((6, 6))
1✔
544
        vscale[3:] *= 2
1✔
545
        vscale[:, 3:] *= 2
1✔
546
        obj = super().__new__(cls, s_array, vscale=vscale)
1✔
547
        return obj.view(cls)
1✔
548

549

550
class ElasticTensorExpansion(TensorCollection):
1✔
551
    """
552
    This class is a sequence of elastic tensors corresponding
553
    to an elastic tensor expansion, which can be used to
554
    calculate stress and energy density and inherits all
555
    of the list-based properties of TensorCollection
556
    (e. g. symmetrization, voigt conversion, etc.)
557
    """
558

559
    def __init__(self, c_list):
1✔
560
        """
561
        Initialization method for ElasticTensorExpansion
562

563
        Args:
564
            c_list (list or tuple): sequence of Tensor inputs
565
                or tensors from which the elastic tensor
566
                expansion is constructed.
567
        """
568
        c_list = [NthOrderElasticTensor(c, check_rank=4 + i * 2) for i, c in enumerate(c_list)]
1✔
569
        super().__init__(c_list)
1✔
570

571
    @classmethod
1✔
572
    def from_diff_fit(cls, strains, stresses, eq_stress=None, tol: float = 1e-10, order=3):
1✔
573
        """
574
        Generates an elastic tensor expansion via the fitting function
575
        defined below in diff_fit
576
        """
577
        c_list = diff_fit(strains, stresses, eq_stress, order, tol)
1✔
578
        return cls(c_list)
1✔
579

580
    @property
1✔
581
    def order(self):
1✔
582
        """
583
        Order of the elastic tensor expansion, i. e. the order of the
584
        highest included set of elastic constants
585
        """
586
        return self[-1].order
1✔
587

588
    def calculate_stress(self, strain):
1✔
589
        """
590
        Calculate's a given elastic tensor's contribution to the
591
        stress using Einstein summation
592
        """
593
        return sum(c.calculate_stress(strain) for c in self)
1✔
594

595
    def energy_density(self, strain, convert_GPa_to_eV=True):
1✔
596
        """
597
        Calculates the elastic energy density due to a strain
598
        """
599
        return sum(c.energy_density(strain, convert_GPa_to_eV) for c in self)
1✔
600

601
    def get_ggt(self, n, u):
1✔
602
        """
603
        Gets the Generalized Gruneisen tensor for a given
604
        third-order elastic tensor expansion.
605

606
        Args:
607
            n (3x1 array-like): normal mode direction
608
            u (3x1 array-like): polarization direction
609
        """
610
        gk = self[0].einsum_sequence([n, u, n, u])
1✔
611
        result = -(
1✔
612
            2 * gk * np.outer(u, u) + self[0].einsum_sequence([n, n]) + self[1].einsum_sequence([n, u, n, u])
613
        ) / (2 * gk)
614
        return result
1✔
615

616
    def get_tgt(self, temperature=None, structure=None, quad=None):
1✔
617
        """
618
        Gets the thermodynamic Gruneisen tensor (TGT) by via an
619
        integration of the GGT weighted by the directional heat
620
        capacity.
621

622
        See refs:
623
            R. N. Thurston and K. Brugger, Phys. Rev. 113, A1604 (1964).
624
            K. Brugger Phys. Rev. 137, A1826 (1965).
625

626
        Args:
627
            temperature (float): Temperature in kelvin, if not specified
628
                will return non-cv-normalized value
629
            structure (float): Structure to be used in directional heat
630
                capacity determination, only necessary if temperature
631
                is specified
632
            quad (dict): quadrature for integration, should be
633
                dictionary with "points" and "weights" keys defaults
634
                to quadpy.sphere.Lebedev(19) as read from file
635
        """
636
        if temperature and not structure:
1✔
637
            raise ValueError("If using temperature input, you must also include structure")
×
638

639
        quad = quad or DEFAULT_QUAD
1✔
640
        points = quad["points"]
1✔
641
        weights = quad["weights"]
1✔
642
        num, denom, c = np.zeros((3, 3)), 0, 1
1✔
643
        for p, w in zip(points, weights):
1✔
644
            gk = ElasticTensor(self[0]).green_kristoffel(p)
1✔
645
            rho_wsquareds, us = np.linalg.eigh(gk)
1✔
646
            us = [u / np.linalg.norm(u) for u in np.transpose(us)]
1✔
647
            for u in us:
1✔
648
                # TODO: this should be benchmarked
649
                if temperature:
1✔
650
                    c = self.get_heat_capacity(temperature, structure, p, u)
1✔
651
                num += c * self.get_ggt(p, u) * w
1✔
652
                denom += c * w
1✔
653
        return SquareTensor(num / denom)
1✔
654

655
    def get_gruneisen_parameter(self, temperature=None, structure=None, quad=None):
1✔
656
        """
657
        Gets the single average gruneisen parameter from the TGT.
658

659
        Args:
660
            temperature (float): Temperature in kelvin, if not specified
661
                will return non-cv-normalized value
662
            structure (float): Structure to be used in directional heat
663
                capacity determination, only necessary if temperature
664
                is specified
665
            quad (dict): quadrature for integration, should be
666
                dictionary with "points" and "weights" keys defaults
667
                to quadpy.sphere.Lebedev(19) as read from file
668
        """
669
        return np.trace(self.get_tgt(temperature, structure, quad)) / 3.0
1✔
670

671
    def get_heat_capacity(self, temperature, structure: Structure, n, u, cutoff=1e2):
1✔
672
        """
673
        Gets the directional heat capacity for a higher order tensor
674
        expansion as a function of direction and polarization.
675

676
        Args:
677
            temperature (float): Temperature in kelvin
678
            structure (float): Structure to be used in directional heat
679
                capacity determination
680
            n (3x1 array-like): direction for Cv determination
681
            u (3x1 array-like): polarization direction, note that
682
                no attempt for verification of eigenvectors is made
683
            cutoff (float): cutoff for scale of kt / (hbar * omega)
684
                if lower than this value, returns 0
685
        """
686
        k = 1.38065e-23
1✔
687
        kt = k * temperature
1✔
688
        hbar_w = 1.05457e-34 * self.omega(structure, n, u)
1✔
689
        if hbar_w > kt * cutoff:
1✔
690
            return 0.0
1✔
691
        c = k * (hbar_w / kt) ** 2
1✔
692
        c *= np.exp(hbar_w / kt) / (np.exp(hbar_w / kt) - 1) ** 2
1✔
693
        return c * 6.022e23
1✔
694

695
    def omega(self, structure: Structure, n, u):
1✔
696
        """
697
        Finds directional frequency contribution to the heat
698
        capacity from direction and polarization
699

700
        Args:
701
            structure (Structure): Structure to be used in directional heat
702
                capacity determination
703
            n (3x1 array-like): direction for Cv determination
704
            u (3x1 array-like): polarization direction, note that
705
                no attempt for verification of eigenvectors is made
706
        """
707
        l0 = np.dot(np.sum(structure.lattice.matrix, axis=0), n)
1✔
708
        l0 *= 1e-10  # in A
1✔
709
        weight = float(structure.composition.weight) * 1.66054e-27  # in kg
1✔
710
        vol = structure.volume * 1e-30  # in m^3
1✔
711
        vel = (1e9 * self[0].einsum_sequence([n, u, n, u]) / (weight / vol)) ** 0.5
1✔
712
        return vel / l0
1✔
713

714
    def thermal_expansion_coeff(self, structure: Structure, temperature, mode="debye"):
1✔
715
        """
716
        Gets thermal expansion coefficient from third-order constants.
717

718
        Args:
719
            temperature (float): Temperature in kelvin, if not specified
720
                will return non-cv-normalized value
721
            structure (Structure): Structure to be used in directional heat
722
                capacity determination, only necessary if temperature
723
                is specified
724
            mode (str): mode for finding average heat-capacity,
725
                current supported modes are 'debye' and 'dulong-petit'
726
        """
727
        soec = ElasticTensor(self[0])
1✔
728
        v0 = structure.volume * 1e-30 / structure.num_sites
1✔
729
        if mode == "debye":
1✔
730
            td = soec.debye_temperature(structure)
1✔
731
            t_ratio = temperature / td
1✔
732

733
            def integrand(x):
1✔
734
                return (x**4 * np.exp(x)) / (np.exp(x) - 1) ** 2
1✔
735

736
            cv = 9 * 8.314 * t_ratio**3 * quad(integrand, 0, t_ratio**-1)[0]
1✔
737
        elif mode == "dulong-petit":
1✔
738
            cv = 3 * 8.314
1✔
739
        else:
740
            raise ValueError("Mode must be debye or dulong-petit")
×
741
        tgt = self.get_tgt(temperature, structure)
1✔
742
        alpha = np.einsum("ijkl,ij", soec.compliance_tensor, tgt)
1✔
743
        alpha *= cv / (1e9 * v0 * 6.022e23)
1✔
744
        return SquareTensor(alpha)
1✔
745

746
    def get_compliance_expansion(self):
1✔
747
        """
748
        Gets a compliance tensor expansion from the elastic
749
        tensor expansion.
750
        """
751
        # TODO: this might have a general form
752
        if not self.order <= 4:
1✔
753
            raise ValueError("Compliance tensor expansion only supported for fourth-order and lower")
×
754
        ce_exp = [ElasticTensor(self[0]).compliance_tensor]
1✔
755
        einstring = "ijpq,pqrsuv,rskl,uvmn->ijklmn"
1✔
756
        ce_exp.append(np.einsum(einstring, -ce_exp[-1], self[1], ce_exp[-1], ce_exp[-1]))
1✔
757
        if self.order == 4:
1✔
758
            # Four terms in the Fourth-Order compliance tensor
759
            # pylint: disable=E1130
760
            einstring_1 = "pqab,cdij,efkl,ghmn,abcdefgh"
1✔
761
            tensors_1 = [ce_exp[0]] * 4 + [self[-1]]
1✔
762
            temp = -np.einsum(einstring_1, *tensors_1)
1✔
763
            einstring_2 = "pqab,abcdef,cdijmn,efkl"
1✔
764
            einstring_3 = "pqab,abcdef,efklmn,cdij"
1✔
765
            einstring_4 = "pqab,abcdef,cdijkl,efmn"
1✔
766
            for es in [einstring_2, einstring_3, einstring_4]:
1✔
767
                temp -= np.einsum(es, ce_exp[0], self[-2], ce_exp[1], ce_exp[0])
1✔
768
            ce_exp.append(temp)
1✔
769
        return TensorCollection(ce_exp)
1✔
770

771
    def get_strain_from_stress(self, stress):
1✔
772
        """
773
        Gets the strain from a stress state according
774
        to the compliance expansion corresponding to the
775
        tensor expansion.
776
        """
777
        compl_exp = self.get_compliance_expansion()
1✔
778
        strain = 0
1✔
779
        for n, compl in enumerate(compl_exp):
1✔
780
            strain += compl.einsum_sequence([stress] * (n + 1)) / factorial(n + 1)
1✔
781
        return strain
1✔
782

783
    def get_effective_ecs(self, strain, order=2):
1✔
784
        """
785
        Returns the effective elastic constants
786
        from the elastic tensor expansion.
787

788
        Args:
789
            strain (Strain or 3x3 array-like): strain condition
790
                under which to calculate the effective constants
791
            order (int): order of the ecs to be returned
792
        """
793
        ec_sum = 0
1✔
794
        for n, ecs in enumerate(self[order - 2 :]):
1✔
795
            ec_sum += ecs.einsum_sequence([strain] * n) / factorial(n)
1✔
796
        return ec_sum
1✔
797

798
    def get_wallace_tensor(self, tau):
1✔
799
        """
800
        Gets the Wallace Tensor for determining yield strength
801
        criteria.
802

803
        Args:
804
            tau (3x3 array-like): stress at which to evaluate
805
                the wallace tensor
806
        """
807
        b = 0.5 * (
1✔
808
            np.einsum("ml,kn->klmn", tau, np.eye(3))
809
            + np.einsum("km,ln->klmn", tau, np.eye(3))
810
            + np.einsum("nl,km->klmn", tau, np.eye(3))
811
            + np.einsum("kn,lm->klmn", tau, np.eye(3))
812
            + -2 * np.einsum("kl,mn->klmn", tau, np.eye(3))
813
        )
814
        strain = self.get_strain_from_stress(tau)
1✔
815
        b += self.get_effective_ecs(strain)
1✔
816
        return b
1✔
817

818
    def get_symmetric_wallace_tensor(self, tau):
1✔
819
        """
820
        Gets the symmetrized wallace tensor for determining
821
        yield strength criteria.
822

823
        Args:
824
            tau (3x3 array-like): stress at which to evaluate
825
                the wallace tensor.
826
        """
827
        wallace = self.get_wallace_tensor(tau)
1✔
828
        return Tensor(0.5 * (wallace + np.transpose(wallace, [2, 3, 0, 1])))
1✔
829

830
    def get_stability_criteria(self, s, n):
1✔
831
        """
832
        Gets the stability criteria from the symmetric
833
        Wallace tensor from an input vector and stress
834
        value.
835

836
        Args:
837
            s (float): Stress value at which to evaluate
838
                the stability criteria
839
            n (3x1 array-like): direction of the applied
840
                stress
841
        """
842
        n = get_uvec(n)
1✔
843
        stress = s * np.outer(n, n)
1✔
844
        sym_wallace = self.get_symmetric_wallace_tensor(stress)
1✔
845
        return np.linalg.det(sym_wallace.voigt)
1✔
846

847
    def get_yield_stress(self, n):
1✔
848
        """
849
        Gets the yield stress for a given direction
850

851
        Args:
852
            n (3x1 array-like): direction for which to find the
853
                yield stress
854
        """
855
        # TODO: root finding could be more robust
856
        comp = root(self.get_stability_criteria, -1, args=n)
1✔
857
        tens = root(self.get_stability_criteria, 1, args=n)
1✔
858
        return (comp.x, tens.x)
1✔
859

860

861
# TODO: abstract this for other tensor fitting procedures
862
def diff_fit(strains, stresses, eq_stress=None, order=2, tol: float = 1e-10):
1✔
863
    """
864
    nth order elastic constant fitting function based on
865
    central-difference derivatives with respect to distinct
866
    strain states. The algorithm is summarized as follows:
867

868
    1. Identify distinct strain states as sets of indices
869
       for which nonzero strain values exist, typically
870
       [(0), (1), (2), (3), (4), (5), (0, 1) etc.]
871
    2. For each strain state, find and sort strains and
872
       stresses by strain value.
873
    3. Find first, second .. nth derivatives of each stress
874
       with respect to scalar variable corresponding to
875
       the smallest perturbation in the strain.
876
    4. Use the pseudoinverse of a matrix-vector expression
877
       corresponding to the parameterized stress-strain
878
       relationship and multiply that matrix by the respective
879
       calculated first or second derivatives from the
880
       previous step.
881
    5. Place the calculated nth-order elastic
882
       constants appropriately.
883

884
    Args:
885
        order (int): order of the elastic tensor set to return
886
        strains (nx3x3 array-like): Array of 3x3 strains
887
            to use in fitting of ECs
888
        stresses (nx3x3 array-like): Array of 3x3 stresses
889
            to use in fitting ECs. These should be PK2 stresses.
890
        eq_stress (3x3 array-like): stress corresponding to
891
            equilibrium strain (i. e. "0" strain state).
892
            If not specified, function will try to find
893
            the state in the list of provided stresses
894
            and strains. If not found, defaults to 0.
895
        tol (float): value for which strains below
896
            are ignored in identifying strain states.
897

898
    Returns:
899
        Set of tensors corresponding to nth order expansion of
900
        the stress/strain relation
901
    """
902
    strain_state_dict = get_strain_state_dict(strains, stresses, eq_stress=eq_stress, tol=tol, add_eq=True, sort=True)
1✔
903

904
    # Collect derivative data
905
    c_list = []
1✔
906
    dei_dsi = np.zeros((order - 1, 6, len(strain_state_dict)))
1✔
907
    for n, (strain_state, data) in enumerate(strain_state_dict.items()):
1✔
908
        hvec = data["strains"][:, strain_state.index(1)]
1✔
909
        for i in range(1, order):
1✔
910
            coef = get_diff_coeff(hvec, i)
1✔
911
            dei_dsi[i - 1, :, n] = np.dot(coef, data["stresses"])
1✔
912

913
    m, absent = generate_pseudo(list(strain_state_dict), order)
1✔
914
    for i in range(1, order):
1✔
915
        cvec, carr = get_symbol_list(i + 1)
1✔
916
        svec = np.ravel(dei_dsi[i - 1].T)
1✔
917
        cmap = dict(zip(cvec, np.dot(m[i - 1], svec)))
1✔
918
        c_list.append(v_subs(carr, cmap))
1✔
919
    return [Tensor.from_voigt(c) for c in c_list]
1✔
920

921

922
def find_eq_stress(strains, stresses, tol: float = 1e-10):
1✔
923
    """
924
    Finds stress corresponding to zero strain state in stress-strain list
925

926
    Args:
927
        strains (Nx3x3 array-like): array corresponding to strains
928
        stresses (Nx3x3 array-like): array corresponding to stresses
929
        tol (float): tolerance to find zero strain state
930
    """
931
    stress_array = np.array(stresses)
1✔
932
    strain_array = np.array(strains)
1✔
933
    eq_stress = stress_array[np.all(abs(strain_array) < tol, axis=(1, 2))]
1✔
934

935
    if eq_stress.size != 0:
1✔
936
        all_same = (abs(eq_stress - eq_stress[0]) < 1e-8).all()
1✔
937
        if len(eq_stress) > 1 and not all_same:
1✔
938
            raise ValueError(
×
939
                "Multiple stresses found for equilibrium strain"
940
                " state, please specify equilibrium stress or  "
941
                " remove extraneous stresses."
942
            )
943
        eq_stress = eq_stress[0]
1✔
944
    else:
945
        warnings.warn("No eq state found, returning zero voigt stress")
1✔
946
        eq_stress = Stress(np.zeros((3, 3)))
1✔
947
    return eq_stress
1✔
948

949

950
def get_strain_state_dict(strains, stresses, eq_stress=None, tol: float = 1e-10, add_eq=True, sort=True):
1✔
951
    """
952
    Creates a dictionary of voigt-notation stress-strain sets
953
    keyed by "strain state", i. e. a tuple corresponding to
954
    the non-zero entries in ratios to the lowest nonzero value,
955
    e.g. [0, 0.1, 0, 0.2, 0, 0] -> (0,1,0,2,0,0)
956
    This allows strains to be collected in stencils as to
957
    evaluate parameterized finite difference derivatives
958

959
    Args:
960
        strains (Nx3x3 array-like): strain matrices
961
        stresses (Nx3x3 array-like): stress matrices
962
        eq_stress (Nx3x3 array-like): equilibrium stress
963
        tol (float): tolerance for sorting strain states
964
        add_eq (bool): flag for whether to add eq_strain
965
            to stress-strain sets for each strain state
966
        sort (bool): flag for whether to sort strain states
967

968
    Returns:
969
        dict: strain state keys and dictionaries with stress-strain data corresponding to strain state
970
    """
971
    # Recast stress/strains
972
    vstrains = np.array([Strain(s).zeroed(tol).voigt for s in strains])  # pylint: disable=E1101
1✔
973
    vstresses = np.array([Stress(s).zeroed(tol).voigt for s in stresses])  # pylint: disable=E1101
1✔
974
    # Collect independent strain states:
975
    independent = {tuple(np.nonzero(vstrain)[0].tolist()) for vstrain in vstrains}
1✔
976
    strain_state_dict = {}
1✔
977
    if add_eq:
1✔
978
        if eq_stress is not None:
1✔
979
            veq_stress = Stress(eq_stress).voigt
1✔
980
        else:
981
            veq_stress = find_eq_stress(strains, stresses).voigt
1✔
982

983
    for ind in independent:
1✔
984
        # match strains with templates
985
        template = np.zeros(6, dtype=bool)
1✔
986
        np.put(template, ind, True)
1✔
987
        template = np.tile(template, [vstresses.shape[0], 1])
1✔
988
        mode = (template == (np.abs(vstrains) > 1e-10)).all(axis=1)
1✔
989
        mstresses = vstresses[mode]
1✔
990
        mstrains = vstrains[mode]
1✔
991
        # Get "strain state", i.e. ratio of each value to minimum strain
992
        min_nonzero_ind = np.argmin(np.abs(np.take(mstrains[-1], ind)))
1✔
993
        min_nonzero_val = np.take(mstrains[-1], ind)[min_nonzero_ind]
1✔
994
        strain_state = mstrains[-1] / min_nonzero_val
1✔
995
        strain_state = tuple(strain_state)
1✔
996

997
        if add_eq:
1✔
998
            # add zero strain state
999
            mstrains = np.vstack([mstrains, np.zeros(6)])
1✔
1000
            mstresses = np.vstack([mstresses, veq_stress])
1✔
1001
        # sort strains/stresses by strain values
1002
        if sort:
1✔
1003
            mstresses = mstresses[mstrains[:, ind[0]].argsort()]
1✔
1004
            mstrains = mstrains[mstrains[:, ind[0]].argsort()]
1✔
1005
        strain_state_dict[strain_state] = {"strains": mstrains, "stresses": mstresses}
1✔
1006
    return strain_state_dict
1✔
1007

1008

1009
def generate_pseudo(strain_states, order=3):
1✔
1010
    """
1011
    Generates the pseudoinverse for a given set of strains.
1012

1013
    Args:
1014
        strain_states (6xN array like): a list of voigt-notation
1015
            "strain-states", i. e. perturbed indices of the strain
1016
            as a function of the smallest strain e. g. (0, 1, 0, 0, 1, 0)
1017
        order (int): order of pseudoinverse to calculate
1018

1019
    Returns:
1020
        mis: pseudo inverses for each order tensor, these can
1021
            be multiplied by the central difference derivative
1022
            of the stress with respect to the strain state
1023
        absent_syms: symbols of the tensor absent from the PI
1024
            expression
1025
    """
1026
    s = sp.Symbol("s")
1✔
1027
    nstates = len(strain_states)
1✔
1028
    ni = np.array(strain_states) * s
1✔
1029
    mis, absent_syms = [], []
1✔
1030
    for degree in range(2, order + 1):
1✔
1031
        cvec, carr = get_symbol_list(degree)
1✔
1032
        sarr = np.zeros((nstates, 6), dtype=object)
1✔
1033
        for n, strain_v in enumerate(ni):
1✔
1034
            # Get expressions
1035
            exps = carr.copy()
1✔
1036
            for _ in range(degree - 1):
1✔
1037
                exps = np.dot(exps, strain_v)
1✔
1038
            exps /= np.math.factorial(degree - 1)
1✔
1039
            sarr[n] = [sp.diff(exp, s, degree - 1) for exp in exps]
1✔
1040
        svec = sarr.ravel()
1✔
1041
        present_syms = set.union(*(exp.atoms(sp.Symbol) for exp in svec))
1✔
1042
        absent_syms += [set(cvec) - present_syms]
1✔
1043
        m = np.zeros((6 * nstates, len(cvec)))
1✔
1044
        for n, c in enumerate(cvec):
1✔
1045
            m[:, n] = v_diff(svec, c)
1✔
1046
        mis.append(np.linalg.pinv(m))
1✔
1047
    return mis, absent_syms
1✔
1048

1049

1050
def get_symbol_list(rank, dim=6):
1✔
1051
    """
1052
    Returns a symbolic representation of the voigt-notation
1053
    tensor that places identical symbols for entries related
1054
    by index transposition, i. e. C_1121 = C_1211 etc.
1055

1056
    Args:
1057
        dim (int): dimension of matrix/tensor, e. g. 6 for
1058
            voigt notation and 3 for standard
1059
        rank (int): rank of tensor, e. g. 3 for third-order ECs
1060

1061
    Returns:
1062
        c_vec (array): array representing distinct indices
1063
        c_arr (array): array representing tensor with equivalent
1064
            indices assigned as above
1065
    """
1066
    indices = list(itertools.combinations_with_replacement(range(dim), r=rank))
1✔
1067
    c_vec = np.zeros(len(indices), dtype=object)
1✔
1068
    c_arr = np.zeros([dim] * rank, dtype=object)
1✔
1069
    for n, idx in enumerate(indices):
1✔
1070
        c_vec[n] = sp.Symbol("c_" + "".join(map(str, idx)))
1✔
1071
        for perm in itertools.permutations(idx):
1✔
1072
            c_arr[perm] = c_vec[n]
1✔
1073
    return c_vec, c_arr
1✔
1074

1075

1076
def subs(entry, cmap):
1✔
1077
    """
1078
    Sympy substitution function, primarily for the purposes
1079
    of numpy vectorization
1080

1081
    Args:
1082
        entry (symbol or exp): sympy expr to undergo subs
1083
        cmap (dict): map for symbols to values to use in subs
1084

1085
    Returns:
1086
        Evaluated expression with substitution
1087
    """
1088
    return entry.subs(cmap)
1✔
1089

1090

1091
# Vectorized functions
1092
v_subs = np.vectorize(subs)
1✔
1093
v_diff = np.vectorize(sp.diff)
1✔
1094

1095

1096
def get_diff_coeff(hvec, n=1):
1✔
1097
    """
1098
    Helper function to find difference coefficients of an
1099
    derivative on an arbitrary mesh.
1100

1101
    Args:
1102
        hvec (1D array-like): sampling stencil
1103
        n (int): degree of derivative to find
1104
    """
1105
    hvec = np.array(hvec, dtype=np.float_)
1✔
1106
    acc = len(hvec)
1✔
1107
    exp = np.column_stack([np.arange(acc)] * acc)
1✔
1108
    a = np.vstack([hvec] * acc) ** exp
1✔
1109
    b = np.zeros(acc)
1✔
1110
    b[n] = factorial(n)
1✔
1111
    return np.linalg.solve(a, b)
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