minelastic 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- minelastic/__init__.py +17 -0
- minelastic/algorithm.py +163 -0
- minelastic/calculator.py +67 -0
- minelastic/cli.py +145 -0
- minelastic/core.py +749 -0
- minelastic/internal_strain.py +42 -0
- minelastic/io/__init__.py +3 -0
- minelastic/io/abinit.py +181 -0
- minelastic/io/aims.py +61 -0
- minelastic/io/generic.py +42 -0
- minelastic/io/qe.py +413 -0
- minelastic/io/vasp.py +432 -0
- minelastic/ionic.py +202 -0
- minelastic/piezo.py +62 -0
- minelastic/properties.py +40 -0
- minelastic/symmetry.py +163 -0
- minelastic/tensor_form.py +159 -0
- minelastic/voigt.py +93 -0
- minelastic/workflow.py +204 -0
- minelastic-0.1.0.dist-info/METADATA +165 -0
- minelastic-0.1.0.dist-info/RECORD +25 -0
- minelastic-0.1.0.dist-info/WHEEL +5 -0
- minelastic-0.1.0.dist-info/entry_points.txt +2 -0
- minelastic-0.1.0.dist-info/licenses/LICENSE +21 -0
- minelastic-0.1.0.dist-info/top_level.txt +1 -0
minelastic/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 Utkarsh Singh
|
|
3
|
+
"""minelastic: second-order elastic constants from minimal uncoupled stress-strain calculations.
|
|
4
|
+
|
|
5
|
+
The elastic constants C_ij = dsigma_i/de_j are obtained from finite differences of the stress of a few
|
|
6
|
+
strained cells, with the minimal uncoupled strain sets of S. I. Simak and U. Singh, arXiv:2610.11733 (2026),
|
|
7
|
+
for any crystal symmetry. Inputs are generated and outputs read for VASP, Quantum ESPRESSO, ABINIT, FHI-aims
|
|
8
|
+
and any ASE format (generate, collect); ASE calculators are used directly (calculate).
|
|
9
|
+
"""
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
__url__ = "https://github.com/utksi/minelastic"
|
|
12
|
+
|
|
13
|
+
from .core import ElasticResult, ElasticSetup # noqa: E402
|
|
14
|
+
from .calculator import calculate # noqa: E402
|
|
15
|
+
from .workflow import collect, generate # noqa: E402
|
|
16
|
+
|
|
17
|
+
__all__ = ["ElasticSetup", "ElasticResult", "calculate", "generate", "collect", "__version__"]
|
minelastic/algorithm.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 Utkarsh Singh
|
|
3
|
+
"""Minimal uncoupled strain sets (Simak and Singh): the paper's construction and an exhaustive search.
|
|
4
|
+
|
|
5
|
+
A strain pattern is a vector u with support S (the Voigt components j with u_j != 0). Per unit step it
|
|
6
|
+
produces the stresses s_i = sum_j C_ij u_j = sum_p c_p sum_{j in S} (B_p)_ij u_j (C = sum_p c_p B_p, see
|
|
7
|
+
tensor_form). For generic amplitudes, s_i depends on constant p exactly when (B_p)_ij != 0 for some j in S.
|
|
8
|
+
s_i is uncoupled if it depends on one constant only; that constant then follows from s_i alone. A set of
|
|
9
|
+
patterns is complete if every constant is uncoupled in at least one of its stress components.
|
|
10
|
+
|
|
11
|
+
paper_greedy is the iterative construction of the paper (Sec. III); exact_minimum searches all
|
|
12
|
+
combinations of the 63 supports for the smallest complete set.
|
|
13
|
+
"""
|
|
14
|
+
from itertools import combinations, product
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
_TOL = 1e-8
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def single_constant_rows(form, support):
|
|
22
|
+
"""[(i, p)]: the stress components i that, for generic amplitudes on the support S, depend on exactly
|
|
23
|
+
one constant p, i.e. (B_q)_ij != 0 for some j in S only for q = p."""
|
|
24
|
+
out = []
|
|
25
|
+
for i in range(6):
|
|
26
|
+
involved = [p for p in range(form.n) if any(abs(form.basis[p, i, j]) > _TOL for j in support)]
|
|
27
|
+
if len(involved) == 1:
|
|
28
|
+
out.append((i, involved[0]))
|
|
29
|
+
return out
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def extractable(form, support):
|
|
33
|
+
"""The set of constants p that are uncoupled in some stress component of a pattern on the support."""
|
|
34
|
+
return {p for _, p in single_constant_rows(form, support)}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def reduced_auxiliary(form):
|
|
38
|
+
"""Reduced auxiliary matrix of the paper (Sec. III B).
|
|
39
|
+
|
|
40
|
+
A_ij = 1 where C_ij is not identically zero, else 0. Row i is set to zero when the diagonal entry C_ii is
|
|
41
|
+
not +-1 times a single constant (a dependent diagonal such as C66 = (c11 - c12)/2), or when its constant
|
|
42
|
+
already appears on an earlier diagonal (an equivalent row, such as C22 = c11).
|
|
43
|
+
"""
|
|
44
|
+
A = form.nonzero.astype(int)
|
|
45
|
+
seen = set()
|
|
46
|
+
for i in range(6):
|
|
47
|
+
d = form.entry(i, i)
|
|
48
|
+
nz = np.flatnonzero(np.abs(d) > _TOL)
|
|
49
|
+
if len(nz) != 1 or abs(abs(d[nz[0]]) - 1) > _TOL or nz[0] in seen:
|
|
50
|
+
A[i, :] = 0 # dependent diagonal (e.g. c66 = (c11-c12)/2) or equivalent to an earlier row
|
|
51
|
+
else:
|
|
52
|
+
seen.add(nz[0])
|
|
53
|
+
return A
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def paper_greedy(form):
|
|
57
|
+
"""Strain supports from the iterative construction of the paper (Sec. III C).
|
|
58
|
+
|
|
59
|
+
Start from the reduced auxiliary matrix A and the candidate support b (b_j = 1 for the nonzero rows of A).
|
|
60
|
+
While some component of v = A b exceeds 1 (row i would respond to more than one strained component),
|
|
61
|
+
remove the highest such index j from b and zero row j. The remaining b is a pattern; its rows are removed
|
|
62
|
+
from A and from the candidates, and the procedure repeats until A is zero. Stops early, without a
|
|
63
|
+
complete set, when no component survives (possible in a non-standard frame); the caller then uses
|
|
64
|
+
exact_minimum.
|
|
65
|
+
"""
|
|
66
|
+
A = reduced_auxiliary(form)
|
|
67
|
+
b_in = (A.sum(axis=1) > 0).astype(int)
|
|
68
|
+
supports = []
|
|
69
|
+
while A.any():
|
|
70
|
+
At, b = A.copy(), b_in.copy()
|
|
71
|
+
while True:
|
|
72
|
+
v = At @ b
|
|
73
|
+
bad = np.flatnonzero(v > 1)
|
|
74
|
+
if bad.size == 0:
|
|
75
|
+
break
|
|
76
|
+
j = bad.max()
|
|
77
|
+
b[j] = 0
|
|
78
|
+
At[j, :] = 0
|
|
79
|
+
if not b.any():
|
|
80
|
+
break # no progress possible (non-standard frame); caller falls back
|
|
81
|
+
supports.append(tuple(int(j) for j in np.flatnonzero(b)))
|
|
82
|
+
A[b == 1, :] = 0
|
|
83
|
+
b_in[b == 1] = 0
|
|
84
|
+
return supports
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def covers(form, supports):
|
|
88
|
+
"""True if the union of extractable(S) over the supports contains every constant."""
|
|
89
|
+
got = set()
|
|
90
|
+
for s in supports:
|
|
91
|
+
got |= extractable(form, s)
|
|
92
|
+
return got == set(range(form.n))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def exact_minimum(form, max_patterns=6):
|
|
96
|
+
"""A complete set of supports of minimum size, by exhaustive search.
|
|
97
|
+
|
|
98
|
+
Each of the 63 nonempty supports S is assigned X(S) = extractable(S). A support is dropped if another
|
|
99
|
+
support S2 has X(S) a subset of X(S2) and no more components (one of equal ones is kept). For
|
|
100
|
+
k = 1, 2, ..., max_patterns all
|
|
101
|
+
k-combinations are tested for complete coverage; among those of the smallest k, the one with the
|
|
102
|
+
fewest strained components in total (least symmetry lowering, then lexicographic) is returned.
|
|
103
|
+
"""
|
|
104
|
+
target = set(range(form.n))
|
|
105
|
+
X = {}
|
|
106
|
+
for mask in range(1, 64):
|
|
107
|
+
s = tuple(j for j in range(6) if mask >> j & 1)
|
|
108
|
+
e = frozenset(extractable(form, s))
|
|
109
|
+
if e:
|
|
110
|
+
X[s] = e
|
|
111
|
+
# WHY: a support is never needed if another one extracts at least the same constants with no more strained
|
|
112
|
+
# components (dropping only such supports keeps both the count and the tie-break exact)
|
|
113
|
+
order = sorted(X, key=lambda s: (len(s), s))
|
|
114
|
+
cands = [s for n, s in enumerate(order)
|
|
115
|
+
if not any(X[s] <= X[s2] and len(s2) <= len(s) and (X[s] < X[s2] or len(s2) < len(s) or m < n)
|
|
116
|
+
for m, s2 in enumerate(order) if s2 != s)]
|
|
117
|
+
for k in range(1, max_patterns + 1):
|
|
118
|
+
best = None
|
|
119
|
+
for combo in combinations(cands, k):
|
|
120
|
+
if set().union(*(X[s] for s in combo)) == target:
|
|
121
|
+
key = (sum(len(s) for s in combo), combo)
|
|
122
|
+
if best is None or key < best[0]:
|
|
123
|
+
best = (key, combo)
|
|
124
|
+
if best is not None:
|
|
125
|
+
return list(best[1])
|
|
126
|
+
raise RuntimeError("no complete set of uncoupled strain patterns exists")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def choose_amplitudes(form, support):
|
|
130
|
+
"""Pattern u on the support, first component 1, with every single-constant coefficient (B_p)_i . u nonzero.
|
|
131
|
+
|
|
132
|
+
For generic amplitudes each single-constant row (i, p) has a nonzero coefficient, but special amplitudes
|
|
133
|
+
can cancel it (e.g. c16 (u_1 - u_2) for u_1 = u_2). Amplitudes +-1 are tried first (all positive first),
|
|
134
|
+
then +-1 and +-2. RuntimeError if none keeps all relations.
|
|
135
|
+
"""
|
|
136
|
+
rows = single_constant_rows(form, support)
|
|
137
|
+
for values in ((1.0, -1.0), (1.0, -1.0, 2.0, -2.0)):
|
|
138
|
+
for amps in product(values, repeat=len(support) - 1):
|
|
139
|
+
u = np.zeros(6)
|
|
140
|
+
u[list(support)] = (1.0,) + amps
|
|
141
|
+
if all(abs(form.basis[p, i] @ u) > _TOL for i, p in rows):
|
|
142
|
+
return u
|
|
143
|
+
raise RuntimeError(f"no amplitudes on support {support} keep all single-constant relations")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def relations(form, pattern):
|
|
147
|
+
"""The six relations s_i = sum_p a_ip c_p of a pattern u (per unit step), a_ip = (B_p)_i . u.
|
|
148
|
+
|
|
149
|
+
One dict per row i: row, coef = (a_ip)_p, and kind 'zero' (all a_ip = 0), 'single' (one nonzero a_ip;
|
|
150
|
+
then also constant = p and factor = a_ip, so that s_i = factor * c_p) or 'coupled'.
|
|
151
|
+
"""
|
|
152
|
+
u = np.asarray(pattern, float)
|
|
153
|
+
out = []
|
|
154
|
+
for i in range(6):
|
|
155
|
+
a = form.basis[:, i, :] @ u
|
|
156
|
+
nz = np.flatnonzero(np.abs(a) > _TOL)
|
|
157
|
+
if nz.size == 0:
|
|
158
|
+
out.append(dict(row=i, kind="zero", coef=a))
|
|
159
|
+
elif nz.size == 1:
|
|
160
|
+
out.append(dict(row=i, kind="single", coef=a, constant=int(nz[0]), factor=float(a[nz[0]])))
|
|
161
|
+
else:
|
|
162
|
+
out.append(dict(row=i, kind="coupled", coef=a))
|
|
163
|
+
return out
|
minelastic/calculator.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 Utkarsh Singh
|
|
3
|
+
"""Elastic constants with an ASE calculator: every state of an ElasticSetup computed in memory.
|
|
4
|
+
|
|
5
|
+
ASE returns the stress in eV/A^3, tension positive, Voigt order xx yy zz yz xz xy; it is converted to GPa
|
|
6
|
+
(x 160.2177) and passed to ElasticSetup.compute with the forces of the clamped cells and, if a dipole function
|
|
7
|
+
is given, the dipole moment of every cell.
|
|
8
|
+
"""
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from .core import ElasticSetup
|
|
12
|
+
|
|
13
|
+
EV_PER_A3_IN_GPA = 160.21766208
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def calculate(atoms, calc, relax_ions=False, method="uncoupled", fmax=1e-4, steps=500, optimizer=None,
|
|
17
|
+
dipole=None, **setup_kwargs):
|
|
18
|
+
"""Elastic tensor of atoms from the stresses of every strained cell, computed with an ASE calculator.
|
|
19
|
+
|
|
20
|
+
Builds ElasticSetup(atoms, **setup_kwargs), computes the stress (eV/A^3 from calc, converted to GPa,
|
|
21
|
+
tension positive) and the forces (eV/A) of every state with calc, and returns ElasticSetup.compute of them.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
atoms : ase.Atoms
|
|
26
|
+
Reference structure, relaxed and stress-free for the textbook constants.
|
|
27
|
+
calc : ase calculator
|
|
28
|
+
Calculator that returns the stress; it is attached to every strained cell.
|
|
29
|
+
relax_ions : bool
|
|
30
|
+
Relax the atoms of each strained cell at fixed cell (optimizer, default BFGS, to fmax eV/A within steps
|
|
31
|
+
steps); the constants are then the relaxed-ion constants and no forces are passed on. Otherwise the
|
|
32
|
+
cells are clamped and their forces give the internal-strain tensor.
|
|
33
|
+
method : str
|
|
34
|
+
Extraction method of ElasticSetup.compute: 'uncoupled', 'designated' or 'lstsq'.
|
|
35
|
+
fmax, steps, optimizer :
|
|
36
|
+
Convergence (eV/A), step limit and ASE optimizer class of the relaxations.
|
|
37
|
+
dipole : callable, optional
|
|
38
|
+
Function of an Atoms object (calculator attached, after any relaxation) that returns the dipole moment
|
|
39
|
+
of the cell in e*Angstrom, physical sign. It gives the piezoelectric tensor e = dP/de (clamped-ion for
|
|
40
|
+
clamped cells, relaxed-ion with relax_ions) and, with ionic=True, the Born charges, the total e and d.
|
|
41
|
+
**setup_kwargs :
|
|
42
|
+
Options of ElasticSetup: scheme, delta, stencil, symprec, frame, ionic, displacement.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
ElasticResult
|
|
47
|
+
With ionic=True, result.C_relaxed is the relaxed-ion stiffness from the clamped cells and the
|
|
48
|
+
displacements.
|
|
49
|
+
"""
|
|
50
|
+
setup = ElasticSetup(atoms, **setup_kwargs)
|
|
51
|
+
if relax_ions and setup.ionic:
|
|
52
|
+
raise ValueError("ionic=True gives the relaxed-ion constants from clamped cells; do not combine it with "
|
|
53
|
+
"relax_ions=True")
|
|
54
|
+
stresses, forces, dipoles = {}, {}, {}
|
|
55
|
+
for st in setup.states:
|
|
56
|
+
at = setup.strained_atoms(st)
|
|
57
|
+
at.calc = calc
|
|
58
|
+
if relax_ions:
|
|
59
|
+
from ase.optimize import BFGS
|
|
60
|
+
(optimizer or BFGS)(at, logfile=None).run(fmax=fmax, steps=steps)
|
|
61
|
+
# ASE stress: eV/A^3, xx yy zz yz xz xy, positive in tension
|
|
62
|
+
stresses[st["id"]] = at.get_stress(voigt=True) * EV_PER_A3_IN_GPA
|
|
63
|
+
forces[st["id"]] = at.get_forces()
|
|
64
|
+
if dipole is not None:
|
|
65
|
+
dipoles[st["id"]] = np.asarray(dipole(at), float)
|
|
66
|
+
return setup.compute(stresses, forces=None if relax_ions else forces, method=method,
|
|
67
|
+
polarization=dipoles if dipole is not None else None)
|
minelastic/cli.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 Utkarsh Singh
|
|
3
|
+
"""Command line interface: minelastic info | generate | collect | vasp-tensor.
|
|
4
|
+
|
|
5
|
+
info prints the symmetry, tensor form and strain set of a structure; generate and collect call
|
|
6
|
+
workflow.generate and workflow.collect; vasp-tensor converts a tensor printed by VASP (IBRION = 6) to GPa in
|
|
7
|
+
Voigt order and adds the Born stability check and the Voigt-Reuss-Hill moduli.
|
|
8
|
+
"""
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
from .core import ElasticSetup
|
|
17
|
+
from .properties import born_stable, vrh
|
|
18
|
+
from .workflow import CODES, collect, generate, read_structure
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _common(p):
|
|
22
|
+
"""Options of the strain set shared by info and generate."""
|
|
23
|
+
p.add_argument("--scheme", default="minimal", choices=["minimal", "asess"],
|
|
24
|
+
help="minimal uncoupled set (default) or the six single-component strains")
|
|
25
|
+
p.add_argument("--delta", type=float, default=0.01, help="strain step (default 0.01)")
|
|
26
|
+
p.add_argument("--stencil", default=None, choices=["central", "four-point", "forward"],
|
|
27
|
+
help="finite differences: four-point (default; -2, -1, 1, 2 steps) or central (+-1 step; the "
|
|
28
|
+
"default with --md) or forward")
|
|
29
|
+
p.add_argument("--symprec", type=float, default=1e-5, help="spglib tolerance in Angstrom")
|
|
30
|
+
p.add_argument("--frame", default="auto", choices=["auto", "input", "standard"],
|
|
31
|
+
help="orientation in which the strains are applied (default: input if it is standard)")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main(argv=None):
|
|
35
|
+
"""Parse the command line and run the subcommand; returns the exit status 0."""
|
|
36
|
+
ap = argparse.ArgumentParser(
|
|
37
|
+
prog="minelastic", epilog="Documentation: https://utksi.github.io/minelastic/",
|
|
38
|
+
description="Elastic constants from the minimal uncoupled stress-strain sets. info shows the symmetry and "
|
|
39
|
+
"the strain set of a structure, generate writes the inputs of the strained cells, collect reads "
|
|
40
|
+
"the finished calculations and prints the elastic tensor, and vasp-tensor analyses the tensor "
|
|
41
|
+
"that VASP prints for IBRION = 6.")
|
|
42
|
+
ap.add_argument("--version", action="version", version=__version__)
|
|
43
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
44
|
+
|
|
45
|
+
p = sub.add_parser("info", help="symmetry, tensor form and strain set of a structure")
|
|
46
|
+
p.add_argument("structure")
|
|
47
|
+
p.add_argument("--code", default="generic", choices=sorted(CODES),
|
|
48
|
+
help="how to read the structure (template dir for vasp/aims/abinit, pw.in for qe)")
|
|
49
|
+
p.add_argument("--format", default=None, help="ASE format for --code generic")
|
|
50
|
+
_common(p)
|
|
51
|
+
|
|
52
|
+
p = sub.add_parser("generate", help="write the strained inputs")
|
|
53
|
+
p.add_argument("code", choices=sorted(CODES))
|
|
54
|
+
p.add_argument("template", help="vasp/aims/abinit: directory with the reference inputs; qe: pw.x input; "
|
|
55
|
+
"generic: structure file")
|
|
56
|
+
p.add_argument("-o", "--out", default="elastic", help="output directory (default ./elastic)")
|
|
57
|
+
p.add_argument("--relax-ions", action="store_true", help="relax the ions in each strained cell")
|
|
58
|
+
p.add_argument("--format", default=None, help="generic: ASE format of the structure files")
|
|
59
|
+
p.add_argument("--filename", default=None, help="generic: name of the structure file in each directory")
|
|
60
|
+
p.add_argument("--extra", nargs="*", default=(), help="generic: further files copied into each directory")
|
|
61
|
+
p.add_argument("--potcar", default="copy", choices=["copy", "link"], help="vasp: copy or symlink POTCAR")
|
|
62
|
+
p.add_argument("--fix-incar", action="store_true",
|
|
63
|
+
help="vasp: apply all recommended settings that the INCAR does not meet (EDIFF, ENCUT, PREC, "
|
|
64
|
+
"LREAL); without it they are only reported")
|
|
65
|
+
p.add_argument("--ionic", action="store_true",
|
|
66
|
+
help="also displace the symmetry-irreducible atoms (relaxed-ion tensors, Born charges and "
|
|
67
|
+
"piezoelectric response from clamped cells, as VASP IBRION=6)")
|
|
68
|
+
p.add_argument("--displacement", type=float, default=0.01, help="--ionic: atomic step in Angstrom")
|
|
69
|
+
p.add_argument("--md", action="store_true",
|
|
70
|
+
help="finite T: fixed-cell molecular dynamics in each strained cell, as set up in the template "
|
|
71
|
+
"(vasp: IBRION = 0; qe: calculation = 'md')")
|
|
72
|
+
_common(p)
|
|
73
|
+
|
|
74
|
+
p = sub.add_parser("collect", help="elastic constants from the finished calculations")
|
|
75
|
+
p.add_argument("directory", nargs="?", default="elastic")
|
|
76
|
+
p.add_argument("--method", default="uncoupled", choices=["uncoupled", "designated", "lstsq"])
|
|
77
|
+
p.add_argument("--output", default=None, help="name of the output file in each directory")
|
|
78
|
+
p.add_argument("--format", default=None, help="generic: ASE format of the output file")
|
|
79
|
+
p.add_argument("--md", action="store_true", help="average the stress over the ionic steps (finite T)")
|
|
80
|
+
p.add_argument("--skip", type=int, default=0, help="--md: steps discarded for equilibration")
|
|
81
|
+
p.add_argument("--samples", default=None,
|
|
82
|
+
help="glob of per-snapshot sub-directories in each state directory (e.g. 'conf-*'); "
|
|
83
|
+
"their stresses are averaged")
|
|
84
|
+
p.add_argument("--kinetic-temperature", type=float, default=None,
|
|
85
|
+
help="add the ideal-gas term n kB T (stresses without kinetic part, e.g. VASP MD)")
|
|
86
|
+
p.add_argument("--internal-strain", action="store_true", help="print the internal strain tensor")
|
|
87
|
+
p.add_argument("--piezo", action="store_true",
|
|
88
|
+
help="piezoelectric tensor from the Berry-phase dipoles of the strained cells (VASP LCALCPOL)")
|
|
89
|
+
p.add_argument("--born", default=None, metavar="OUTCAR",
|
|
90
|
+
help="Born charges and clamped-ion piezoelectric tensor from a VASP LEPSILON/LCALCEPS run of "
|
|
91
|
+
"the reference structure (with --ionic data)")
|
|
92
|
+
p.add_argument("--stress-measure", default="cauchy", choices=["cauchy", "kirchhoff"],
|
|
93
|
+
help="differentiate the Cauchy stress (default) or J*sigma (as VASP IBRION=6)")
|
|
94
|
+
|
|
95
|
+
p = sub.add_parser("vasp-tensor", help="analyse the elastic tensor printed by VASP IBRION=6")
|
|
96
|
+
p.add_argument("outcar")
|
|
97
|
+
p.add_argument("--which", default="total", choices=["total", "clamped", "ionic"])
|
|
98
|
+
|
|
99
|
+
a = ap.parse_args(argv)
|
|
100
|
+
kw = dict(scheme=a.scheme, delta=a.delta, stencil=a.stencil, symprec=a.symprec, frame=a.frame) \
|
|
101
|
+
if a.cmd in ("info", "generate") else {}
|
|
102
|
+
|
|
103
|
+
if a.cmd == "info":
|
|
104
|
+
atoms = read_structure(a.code, a.structure, a.format)
|
|
105
|
+
print(ElasticSetup(atoms, **{**kw, "stencil": a.stencil or "four-point"}).describe())
|
|
106
|
+
elif a.cmd == "generate":
|
|
107
|
+
if a.fix_incar and a.code != "vasp":
|
|
108
|
+
ap.error("--fix-incar is for vasp")
|
|
109
|
+
extra = dict(potcar=a.potcar, fix_incar=a.fix_incar) if a.code == "vasp" else {}
|
|
110
|
+
setup, notes = generate(a.code, a.template, a.out, relax_ions=a.relax_ions, fmt=a.format,
|
|
111
|
+
filename=a.filename, extra_files=a.extra, ionic=a.ionic,
|
|
112
|
+
displacement=a.displacement, md=a.md, **kw, **extra)
|
|
113
|
+
print(setup.describe())
|
|
114
|
+
print(f"\n{len(setup.states)} calculations written to {a.out}/ (list in {a.out}/STATES)")
|
|
115
|
+
for n in notes:
|
|
116
|
+
print("NOTE:", n)
|
|
117
|
+
elif a.cmd == "collect":
|
|
118
|
+
r = collect(a.directory, method=a.method, output=a.output, fmt=a.format, md=a.md, skip=a.skip,
|
|
119
|
+
kinetic_temperature=a.kinetic_temperature, samples=a.samples,
|
|
120
|
+
stress_measure=a.stress_measure, polarization=a.piezo, born_file=a.born)
|
|
121
|
+
print(r.summary())
|
|
122
|
+
if a.internal_strain and r.internal_strain is not None:
|
|
123
|
+
print("internal strain tensor dF/de (eV/A), columns xx yy zz yz xz xy:")
|
|
124
|
+
for n, L in enumerate(r.internal_strain):
|
|
125
|
+
for i, ax in enumerate("xyz"):
|
|
126
|
+
print(f" ion {n + 1:3d} {ax} " + " ".join(f"{v:10.4f}" for v in L[i]))
|
|
127
|
+
print(f"\nwritten {a.directory}/elastic.json")
|
|
128
|
+
elif a.cmd == "vasp-tensor":
|
|
129
|
+
from .io.vasp import read_elastic_tensor
|
|
130
|
+
which = {"total": "TOTAL ELASTIC MODULI", "clamped": "SYMMETRIZED ELASTIC MODULI",
|
|
131
|
+
"ionic": "ELASTIC MODULI CONTR FROM IONIC RELAXATION"}[a.which]
|
|
132
|
+
C = read_elastic_tensor(a.outcar, which)
|
|
133
|
+
print(f"{which} (GPa, Voigt xx yy zz yz xz xy):")
|
|
134
|
+
for row in C:
|
|
135
|
+
print(" " + " ".join(f"{v:9.2f}" for v in row))
|
|
136
|
+
if a.which != "ionic":
|
|
137
|
+
stable, ev = born_stable(C)
|
|
138
|
+
print(f"Born stable: {stable}")
|
|
139
|
+
if stable:
|
|
140
|
+
print(json.dumps({k: round(v, 3) for k, v in vrh(C).items()}))
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
sys.exit(main())
|