lineshape_tools 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.
- lineshape_tools/__init__.py +7 -0
- lineshape_tools/__main__.py +50 -0
- lineshape_tools/cli.py +965 -0
- lineshape_tools/constants.py +4 -0
- lineshape_tools/lineshape.py +279 -0
- lineshape_tools/phonon.py +77 -0
- lineshape_tools/plot.py +294 -0
- lineshape_tools-0.1.0.dist-info/METADATA +77 -0
- lineshape_tools-0.1.0.dist-info/RECORD +12 -0
- lineshape_tools-0.1.0.dist-info/WHEEL +4 -0
- lineshape_tools-0.1.0.dist-info/entry_points.txt +5 -0
- lineshape_tools-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Implements functionality to evaluate optical lineshapes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import NamedTuple
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numba import njit
|
|
10
|
+
|
|
11
|
+
from lineshape_tools.constants import kelvin2eV, omega2eV
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_Stot(dq: np.ndarray, omega: np.ndarray) -> float:
|
|
17
|
+
"""Calculate the Huang-Rhys factor Stot.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
dq (np.ndarray): mass-weighted displacement vector in normal mode basis in (amu^{1/2} Ang).
|
|
21
|
+
omega (np.ndarray): normal mode phonon frequencies (in eV/amu/Ang^2).
|
|
22
|
+
"""
|
|
23
|
+
return np.sum(0.5 * omega * dq**2 / omega2eV)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@njit(cache=True, fastmath=True, error_model="numpy")
|
|
27
|
+
def gaussian(x: np.ndarray, s: float) -> np.ndarray:
|
|
28
|
+
"""Evaluate the Gaussian function with smearing s."""
|
|
29
|
+
return np.exp(-(x**2) / 2 / s**2) / np.sqrt(2 * np.pi) / s
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@njit(cache=True, fastmath=True, error_model="numpy")
|
|
33
|
+
def lorentzian(x: np.ndarray, g: float) -> np.ndarray:
|
|
34
|
+
"""Evaluate the Lorentzian function with broadening g."""
|
|
35
|
+
return g / np.pi / (x**2 + g**2)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Broadening(NamedTuple):
|
|
39
|
+
gamma_zpl: float
|
|
40
|
+
sigma_zpl: float
|
|
41
|
+
method_psb: np.ndarray
|
|
42
|
+
value_psb: np.ndarray
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def create(
|
|
46
|
+
cls,
|
|
47
|
+
omega: np.ndarray,
|
|
48
|
+
gamma_zpl: float = 0.001,
|
|
49
|
+
sigma_zpl: float = 0.0,
|
|
50
|
+
sigma_psb: tuple[float, float] = (0.005, 0.001),
|
|
51
|
+
gamma_lvm: float = 0.001,
|
|
52
|
+
ipr_cut: float = 10.0,
|
|
53
|
+
ipr: np.ndarray | None = None,
|
|
54
|
+
) -> Broadening:
|
|
55
|
+
"""Create an instance of Broadening.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
omega (np.ndarray): normal mode phonon frequencies (in eV/amu/Ang^2).
|
|
59
|
+
gamma_zpl (float): Lorentzian broadening in the ZPL to capture homogeneous broadening.
|
|
60
|
+
sigma_zpl (float): Gaussian broadening in the ZPL to capture inhomogeneous broadening.
|
|
61
|
+
sigma_psb (float, float): Gaussian broadening used to broaden the partial Huang-Rhys
|
|
62
|
+
factors. The broadening factor is linearly interpolated from sigma_psb[0] at zero
|
|
63
|
+
frequency to sigma_psb[1] at the highest (non-LVM) frequency.
|
|
64
|
+
gamma_lvm (float): Lorentzian broadening applied to local vibrational modes identified
|
|
65
|
+
by their inverse participation ratio.
|
|
66
|
+
ipr_cut (float): Local vibrational modes are identified when their ipr < ipr_cut.
|
|
67
|
+
ipr (np.ndarray): array of inverse participation ratios for each mode.
|
|
68
|
+
"""
|
|
69
|
+
w_k = omega2eV * omega
|
|
70
|
+
|
|
71
|
+
method_psb = np.zeros(omega.shape[0], dtype=np.uint8)
|
|
72
|
+
|
|
73
|
+
if ipr is not None:
|
|
74
|
+
mask = ipr < ipr_cut
|
|
75
|
+
w_max = w_k[~mask].max()
|
|
76
|
+
else:
|
|
77
|
+
mask = None
|
|
78
|
+
w_max = w_k.max()
|
|
79
|
+
|
|
80
|
+
x = w_k / w_max
|
|
81
|
+
value_psb = sigma_psb[0] * (1 - x) + sigma_psb[1] * x
|
|
82
|
+
|
|
83
|
+
if ipr is not None:
|
|
84
|
+
method_psb[mask] = 1
|
|
85
|
+
value_psb[mask] = gamma_lvm
|
|
86
|
+
|
|
87
|
+
return cls(
|
|
88
|
+
gamma_zpl=gamma_zpl, sigma_zpl=sigma_zpl, method_psb=method_psb, value_psb=value_psb
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@njit(cache=True, fastmath=True, error_model="numpy")
|
|
93
|
+
def _do_compute_phonon_spec_func_zeroT(
|
|
94
|
+
w: np.ndarray,
|
|
95
|
+
dq: np.ndarray,
|
|
96
|
+
omega: np.ndarray,
|
|
97
|
+
broadening: Broadening,
|
|
98
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
99
|
+
"""Compute the phonon spectral function and related quantities at zero T."""
|
|
100
|
+
dw = w[1] - w[0]
|
|
101
|
+
|
|
102
|
+
w_k = omega2eV * omega
|
|
103
|
+
S_k = 0.5 * omega * dq**2 / omega2eV
|
|
104
|
+
|
|
105
|
+
S_w, dos = np.zeros_like(w), np.zeros_like(w)
|
|
106
|
+
for i in range(S_k.shape[0]):
|
|
107
|
+
broad_f = lorentzian if broadening.method_psb[i] == 1 else gaussian
|
|
108
|
+
S_w += S_k[i] * broad_f(w - w_k[i], broadening.value_psb[i])
|
|
109
|
+
dos += broad_f(w - w_k[i], broadening.value_psb[i])
|
|
110
|
+
dos /= S_k.shape[0]
|
|
111
|
+
|
|
112
|
+
S_0 = S_w.sum() * dw
|
|
113
|
+
if not np.isclose(S_k.sum(), S_0, rtol=1e-3):
|
|
114
|
+
print(
|
|
115
|
+
"Warning! Inconsistency in computed HR factors (",
|
|
116
|
+
S_k.sum(),
|
|
117
|
+
S_0,
|
|
118
|
+
"). You may want to decrease sigma.",
|
|
119
|
+
)
|
|
120
|
+
S_t = np.fft.rfft(S_w) * dw
|
|
121
|
+
t = 2 * np.pi * np.fft.rfftfreq(S_w.shape[0]) / dw
|
|
122
|
+
|
|
123
|
+
G_t = np.exp(S_t - S_0 - broadening.gamma_zpl * t - 0.5 * broadening.sigma_zpl**2 * t**2)
|
|
124
|
+
A_w = np.fft.irfft(G_t) / dw
|
|
125
|
+
return dos, S_w, A_w
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@njit(cache=True, fastmath=True, error_model="numpy")
|
|
129
|
+
def _do_compute_phonon_spec_func(
|
|
130
|
+
w: np.ndarray,
|
|
131
|
+
dq: np.ndarray,
|
|
132
|
+
omega: np.ndarray,
|
|
133
|
+
broadening: Broadening,
|
|
134
|
+
T: float,
|
|
135
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
136
|
+
"""Compute the phonon spectral function and related quantities at arbitrary T."""
|
|
137
|
+
dw = w[1] - w[0]
|
|
138
|
+
kT = kelvin2eV * T
|
|
139
|
+
|
|
140
|
+
w_k = omega2eV * omega
|
|
141
|
+
S_k = 0.5 * omega * dq**2 / omega2eV
|
|
142
|
+
|
|
143
|
+
n_k = np.zeros_like(S_k)
|
|
144
|
+
n_k[w_k > 0] = 1 / (np.exp(w_k[w_k > 0] / kT) - 1)
|
|
145
|
+
|
|
146
|
+
S_w, C_w, dos = np.zeros_like(w), np.zeros_like(w), np.zeros_like(w)
|
|
147
|
+
for i in range(S_k.shape[0]):
|
|
148
|
+
broad_f = lorentzian if broadening.method_psb[i] == 1 else gaussian
|
|
149
|
+
S_w += S_k[i] * broad_f(w - w_k[i], broadening.value_psb[i])
|
|
150
|
+
C_w += n_k[i] * S_k[i] * broad_f(w - w_k[i], broadening.value_psb[i])
|
|
151
|
+
dos += broad_f(w - w_k[i], broadening.value_psb[i])
|
|
152
|
+
dos /= S_k.shape[0]
|
|
153
|
+
|
|
154
|
+
S_0, C_0 = (S_w.sum() * dw), (C_w.sum() * dw)
|
|
155
|
+
if not np.isclose(S_k.sum(), S_0, rtol=1e-3):
|
|
156
|
+
print(
|
|
157
|
+
"Warning! Inconsistency in computed HR factors (",
|
|
158
|
+
S_k.sum(),
|
|
159
|
+
S_0,
|
|
160
|
+
"). You may want to decrease sigma.",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
S_t = np.fft.fft(S_w) * dw
|
|
164
|
+
C_t = np.fft.fft(C_w) * dw
|
|
165
|
+
t = 2 * np.pi * np.fft.fftfreq(S_w.shape[0]) / dw
|
|
166
|
+
|
|
167
|
+
G_t = np.exp(
|
|
168
|
+
S_t
|
|
169
|
+
- S_0
|
|
170
|
+
+ C_t
|
|
171
|
+
+ C_t.conj()
|
|
172
|
+
- 2 * C_0
|
|
173
|
+
- broadening.gamma_zpl * np.abs(t)
|
|
174
|
+
- 0.5 * broadening.sigma_zpl**2 * t**2
|
|
175
|
+
)
|
|
176
|
+
A_w = np.fft.ifft(G_t) / dw
|
|
177
|
+
return dos, S_w, A_w.real
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def get_phonon_spec_func(
|
|
181
|
+
dq: np.ndarray,
|
|
182
|
+
omega: np.ndarray,
|
|
183
|
+
broadening: Broadening | None = None,
|
|
184
|
+
resolution: float = 1e-3,
|
|
185
|
+
w_max: float | None = None,
|
|
186
|
+
pad: float = 0.1,
|
|
187
|
+
T: float = 0.0,
|
|
188
|
+
**kwargs,
|
|
189
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
190
|
+
"""Compute the phonon spectral function and related quantities.
|
|
191
|
+
|
|
192
|
+
If broadening is not provided, kwargs will be passed to Broadening.create
|
|
193
|
+
(see :func:`Broadening.create`).
|
|
194
|
+
|
|
195
|
+
When T == 0. (the default), an optimized evaluation of the spectral function is utilized.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
dq (np.ndarray): mass-weighted displacement vector in normal mode basis in (amu^{1/2} Ang).
|
|
199
|
+
omega (np.ndarray): normal mode phonon frequencies (in eV/amu/Ang^2).
|
|
200
|
+
broadening (Broadening): instance of :class:`Broadening`.
|
|
201
|
+
resolution (float): desired energy resolution (in eV) of the calculated spectral functions.
|
|
202
|
+
w_max (np.ndarray): maximum frequency in grid.
|
|
203
|
+
pad (float): energy (in eV) below ZPL to pad the spectral function.
|
|
204
|
+
T (float): Temperature in kelvin.
|
|
205
|
+
**kwargs: keyword arguments passed to :func:`Broadening.create` when broadening is not
|
|
206
|
+
provided as input.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
w (np.ndarray): frequencies at which the functions were evaluated at (ħω)
|
|
210
|
+
dos (np.ndarray): phonon density of states ρ(ħω)
|
|
211
|
+
S_w (np.ndarray): Huang-Rhys spectral density S(ħω)
|
|
212
|
+
A_w (np.ndarray): spectral function A(ħω)
|
|
213
|
+
"""
|
|
214
|
+
broadening = broadening or Broadening.create(omega=omega, **kwargs)
|
|
215
|
+
|
|
216
|
+
if resolution > broadening.gamma_zpl and resolution > broadening.sigma_zpl:
|
|
217
|
+
resolution = max(broadening.gamma_zpl, broadening.sigma_zpl)
|
|
218
|
+
logger.info(f"energy resolution is larger than ZPL broadening, decreasing to {resolution}")
|
|
219
|
+
|
|
220
|
+
w_max = w_max or (pad + max(2 * get_Stot(dq, omega), 3) * omega.max() * omega2eV)
|
|
221
|
+
N = int(w_max / resolution) + 1
|
|
222
|
+
# ensure an even number of grid points for T = 0 evaluation
|
|
223
|
+
if T <= 0.0:
|
|
224
|
+
N += N % 2
|
|
225
|
+
w = np.linspace(0.0, w_max, N)
|
|
226
|
+
if N > 50_000:
|
|
227
|
+
logger.warning(f"large number of grid points {N=}")
|
|
228
|
+
logger.debug(f"made frequency grid with {w_max=} {N=}")
|
|
229
|
+
|
|
230
|
+
if T > 0:
|
|
231
|
+
dos, S_w, A_w = _do_compute_phonon_spec_func(w, dq, omega, broadening, T)
|
|
232
|
+
else:
|
|
233
|
+
dos, S_w, A_w = _do_compute_phonon_spec_func_zeroT(w, dq, omega, broadening)
|
|
234
|
+
|
|
235
|
+
# padding at the end of w was for wrap-around in fft, need to roll arrays
|
|
236
|
+
n_shift = np.sum(w >= w[-1] - pad)
|
|
237
|
+
|
|
238
|
+
dos = np.roll(dos, n_shift)
|
|
239
|
+
S_w = np.roll(S_w, n_shift)
|
|
240
|
+
A_w = np.roll(A_w, n_shift)
|
|
241
|
+
|
|
242
|
+
new_w = np.roll(w, n_shift)
|
|
243
|
+
new_w[:n_shift] -= w[-1]
|
|
244
|
+
|
|
245
|
+
return new_w, dos, S_w, A_w
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def convert_A_to_L(
|
|
249
|
+
w: np.ndarray,
|
|
250
|
+
A: np.ndarray,
|
|
251
|
+
dE: float,
|
|
252
|
+
emission: bool = True,
|
|
253
|
+
norm: str = "area",
|
|
254
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
255
|
+
"""Convert phonon spectral function into a (normalized) optical intensity.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
w (np.ndarray): frequencies (in eV) where A is evaluated.
|
|
259
|
+
A (np.ndarray): spectral function.
|
|
260
|
+
dE (float): energy of the zero-phonon line.
|
|
261
|
+
emission (bool): determines if the intensity corresponds to emission or absorption.
|
|
262
|
+
norm (str): normalization of luminescence (area or max).
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
new_w (np.ndarray): expanded range of frequencies where intensity is evaluated.
|
|
266
|
+
L (np.ndarray): optical intensity L(ħω) on expanded frequencies new_w.
|
|
267
|
+
"""
|
|
268
|
+
new_w = (dE - w) if emission else (dE + w)
|
|
269
|
+
|
|
270
|
+
# remove negative frequencies, they should be superfluous anyway
|
|
271
|
+
tmp_A = A[new_w >= 0.0]
|
|
272
|
+
new_w = new_w[new_w >= 0.0]
|
|
273
|
+
|
|
274
|
+
L = new_w**3 * tmp_A if emission else new_w * tmp_A
|
|
275
|
+
if norm[0].lower() == "m":
|
|
276
|
+
L /= L.max()
|
|
277
|
+
else:
|
|
278
|
+
L /= L.sum() * (w[1] - w[0])
|
|
279
|
+
return new_w, L
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Implements functionality for handling phonons."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from lineshape_tools.constants import omega2eV
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ase.atoms import Atoms
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_ipr(U: np.ndarray) -> np.ndarray:
|
|
17
|
+
"""Evaluate the inverse participation ratio.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
U (np.ndarray): matrix with phonon eigenvectors as columns (see np.linalg.eigh).
|
|
21
|
+
"""
|
|
22
|
+
return 1 / np.sum(
|
|
23
|
+
np.sum(U.reshape((U.shape[0] // 3, 3, U.shape[1])) ** 2, axis=1) ** 2, axis=0
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_disp_vect(atoms0: Atoms, atoms1: Atoms) -> np.ndarray:
|
|
28
|
+
"""Compute smallest displacement vector between two sets of atoms.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
atoms0 (Atoms): first set of atoms
|
|
32
|
+
atoms1 (Atoms): second set of atoms
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
np.ndarray: 3N dimensional displacement vector in Å
|
|
36
|
+
"""
|
|
37
|
+
dx = atoms0.get_scaled_positions() - atoms1.get_scaled_positions()
|
|
38
|
+
dx -= np.round(dx)
|
|
39
|
+
dx = dx @ atoms0.cell
|
|
40
|
+
return dx.flatten()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_dq_vect(atoms0: Atoms, atoms1: Atoms) -> np.ndarray:
|
|
44
|
+
"""Compute smallest mass-weighted displacement vector between two sets of atoms.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
atoms0 (Atoms): first set of atoms
|
|
48
|
+
atoms1 (Atoms): second set of atoms
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
np.ndarray: 3N dimensional displacement vector in amu^{1/2} Å
|
|
52
|
+
"""
|
|
53
|
+
return np.repeat(np.sqrt(atoms0.get_masses()), 3) * get_disp_vect(atoms0, atoms1)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_phonons(
|
|
57
|
+
dynmat: np.ndarray | Path | str, acoustic_tol: float = 5e-4
|
|
58
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
59
|
+
"""Compute the phonon frequencies and eigenvectors from a dynamical matrix.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
dynmat (np.ndarray | Path | str): path to a dynamical matrix in .npz format or a np.ndarray
|
|
63
|
+
corresponding to a dynamical matrix (shape 3N x 3N).
|
|
64
|
+
acoustic_tol (float): tolerance (in eV) to determine acoustic phonon modes.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
omega (np.ndarray): phonon frequencies (in eV/amu/Ang^2).
|
|
68
|
+
U (np.ndarray): matrix with phonon eigenvectors as columns (see np.linalg.eigh).
|
|
69
|
+
"""
|
|
70
|
+
if isinstance(dynmat, Path) or isinstance(dynmat, str):
|
|
71
|
+
H = np.load(dynmat)["H"]
|
|
72
|
+
else:
|
|
73
|
+
H = dynmat
|
|
74
|
+
|
|
75
|
+
omega2, U = np.linalg.eigh(H)
|
|
76
|
+
omega2[omega2 < (acoustic_tol / omega2eV) ** 2] = 0.0
|
|
77
|
+
return np.sqrt(omega2), U
|
lineshape_tools/plot.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""Various plotting utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from lineshape_tools.constants import omega2eV
|
|
9
|
+
from lineshape_tools.lineshape import convert_A_to_L, get_phonon_spec_func
|
|
10
|
+
from lineshape_tools.phonon import get_ipr, get_phonons
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _make_subplots(
|
|
21
|
+
spec_funcs: list[tuple],
|
|
22
|
+
dE: float,
|
|
23
|
+
emission: bool,
|
|
24
|
+
omega_max: float,
|
|
25
|
+
omega_mult: float,
|
|
26
|
+
figsize: tuple[float, float],
|
|
27
|
+
skip_lim_adjust: bool,
|
|
28
|
+
ax=None,
|
|
29
|
+
):
|
|
30
|
+
"""Helper function to make subplot-type plot."""
|
|
31
|
+
if ax is not None:
|
|
32
|
+
fig = ax[0].get_figure()
|
|
33
|
+
else:
|
|
34
|
+
import matplotlib.pyplot as plt
|
|
35
|
+
|
|
36
|
+
fig, ax = plt.subplots(1, 3, figsize=figsize, constrained_layout=True)
|
|
37
|
+
|
|
38
|
+
for spec_func in spec_funcs:
|
|
39
|
+
w, dos, S, tw, L = spec_func
|
|
40
|
+
|
|
41
|
+
if dos is not None:
|
|
42
|
+
ax[0].plot(w, dos)
|
|
43
|
+
|
|
44
|
+
if S is not None:
|
|
45
|
+
ax[1].plot(w, S)
|
|
46
|
+
|
|
47
|
+
if L is not None:
|
|
48
|
+
ax[2].plot(tw, L)
|
|
49
|
+
|
|
50
|
+
if not skip_lim_adjust:
|
|
51
|
+
for a in ax:
|
|
52
|
+
a.set_ylim((0, a.get_ylim()[1]))
|
|
53
|
+
|
|
54
|
+
for a in ax[:2]:
|
|
55
|
+
a.set_xlim((0.0, omega_max + 0.005))
|
|
56
|
+
|
|
57
|
+
if emission:
|
|
58
|
+
ax[2].set_xlim((dE - omega_mult * omega_max - 0.02, dE + 0.02))
|
|
59
|
+
else:
|
|
60
|
+
ax[2].set_xlim((dE - 0.02, dE + omega_mult * omega_max + 0.02))
|
|
61
|
+
|
|
62
|
+
ax[0].set_xlabel(r"$\hbar\omega$ [eV]")
|
|
63
|
+
ax[0].set_ylabel(r"DOS $\rho(\hbar\omega)$ [eV$^{-1}$]")
|
|
64
|
+
ax[1].set_xlabel(r"$\hbar\omega$ [eV]")
|
|
65
|
+
ax[1].set_ylabel(r"Spectral Density $S(\hbar\omega)$ [eV$^{-1}$]")
|
|
66
|
+
ax[2].set_xlabel(r"Energy [eV]")
|
|
67
|
+
if emission:
|
|
68
|
+
ax[2].set_ylabel(r"Luminescence $L(\hbar\omega)$ [eV$^{-1}$]")
|
|
69
|
+
else:
|
|
70
|
+
ax[2].set_ylabel(r"Absorption $L(\hbar\omega)$ [eV$^{-1}$]")
|
|
71
|
+
return fig, ax
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _make_inset(
|
|
75
|
+
spec_funcs: list[tuple],
|
|
76
|
+
dE: float,
|
|
77
|
+
emission: bool,
|
|
78
|
+
omega_max: float,
|
|
79
|
+
omega_mult: float,
|
|
80
|
+
figsize: tuple[float, float],
|
|
81
|
+
skip_lim_adjust: bool,
|
|
82
|
+
ax=None,
|
|
83
|
+
):
|
|
84
|
+
"""Helper function to make inset-type plot."""
|
|
85
|
+
if ax is not None:
|
|
86
|
+
fig = ax[0].get_figure()
|
|
87
|
+
else:
|
|
88
|
+
import matplotlib.pyplot as plt
|
|
89
|
+
|
|
90
|
+
fig, ax1 = plt.subplots(figsize=figsize, constrained_layout=True)
|
|
91
|
+
ax = [ax1, fig.add_axes((0.17, 0.55, 0.4, 0.35))]
|
|
92
|
+
|
|
93
|
+
for spec_func in spec_funcs:
|
|
94
|
+
w, _, S, tw, L = spec_func
|
|
95
|
+
|
|
96
|
+
if L is not None:
|
|
97
|
+
ax[0].plot(tw, L)
|
|
98
|
+
|
|
99
|
+
if S is not None:
|
|
100
|
+
ax[1].plot(w, S)
|
|
101
|
+
|
|
102
|
+
if not skip_lim_adjust:
|
|
103
|
+
for a in ax:
|
|
104
|
+
a.set_ylim((0, a.get_ylim()[1]))
|
|
105
|
+
|
|
106
|
+
ax[1].set_xlim((0.0, omega_max + 0.005))
|
|
107
|
+
|
|
108
|
+
if emission:
|
|
109
|
+
ax[0].set_xlim((dE - omega_mult * omega_max - 0.02, dE + 0.02))
|
|
110
|
+
else:
|
|
111
|
+
ax[0].set_xlim((dE - 0.02, dE + omega_mult * omega_max + 0.02))
|
|
112
|
+
|
|
113
|
+
ax[0].set_xlabel(r"Energy [eV]")
|
|
114
|
+
if emission:
|
|
115
|
+
ax[0].set_ylabel(r"Luminescence $L(\hbar\omega)$ [eV$^{-1}$]")
|
|
116
|
+
else:
|
|
117
|
+
ax[0].set_ylabel(r"Absorption $L(\hbar\omega)$ [eV$^{-1}$]")
|
|
118
|
+
ax[1].set_xlabel(r"$\hbar\omega$ [eV]")
|
|
119
|
+
ax[1].set_ylabel(r"$S(\hbar\omega)$ [eV$^{-1}$]")
|
|
120
|
+
return fig, ax
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _make_single(
|
|
124
|
+
func_type: str,
|
|
125
|
+
spec_funcs: list[tuple],
|
|
126
|
+
dE: float,
|
|
127
|
+
emission: bool,
|
|
128
|
+
omega_max: float,
|
|
129
|
+
omega_mult: float,
|
|
130
|
+
figsize: tuple[float, float],
|
|
131
|
+
skip_lim_adjust: bool,
|
|
132
|
+
ax=None,
|
|
133
|
+
):
|
|
134
|
+
"""Helper function to make a single-function plot."""
|
|
135
|
+
if ax is not None:
|
|
136
|
+
fig = ax.get_figure()
|
|
137
|
+
else:
|
|
138
|
+
import matplotlib.pyplot as plt
|
|
139
|
+
|
|
140
|
+
fig, ax = plt.subplots(figsize=figsize, constrained_layout=True)
|
|
141
|
+
|
|
142
|
+
for spec_func in spec_funcs:
|
|
143
|
+
w, dos, S, tw, L = spec_func
|
|
144
|
+
|
|
145
|
+
if func_type[0].lower() == "d":
|
|
146
|
+
ax.plot(w, dos)
|
|
147
|
+
elif func_type[0].lower() == "s":
|
|
148
|
+
ax.plot(w, S)
|
|
149
|
+
else:
|
|
150
|
+
ax.plot(tw, L)
|
|
151
|
+
|
|
152
|
+
if not skip_lim_adjust:
|
|
153
|
+
ax.set_ylim((0, ax.get_ylim()[1]))
|
|
154
|
+
|
|
155
|
+
if func_type[0].lower() in ("d", "s"):
|
|
156
|
+
ax.set_xlim((0.0, omega_max + 0.005))
|
|
157
|
+
else:
|
|
158
|
+
if emission:
|
|
159
|
+
ax.set_xlim((dE - omega_mult * omega_max - 0.02, dE + 0.02))
|
|
160
|
+
else:
|
|
161
|
+
ax.set_xlim((dE - 0.02, dE + omega_mult * omega_max + 0.02))
|
|
162
|
+
|
|
163
|
+
if func_type[0].lower() in ("d", "s"):
|
|
164
|
+
ax.set_xlabel(r"$\hbar\omega$ [eV]")
|
|
165
|
+
else:
|
|
166
|
+
ax.set_xlabel(r"Energy [eV]")
|
|
167
|
+
|
|
168
|
+
if func_type[0].lower() == "d":
|
|
169
|
+
ax.set_ylabel(r"DOS $\rho(\hbar\omega)$ [eV$^{-1}$]")
|
|
170
|
+
elif func_type[0].lower() == "s":
|
|
171
|
+
ax.set_ylabel(r"Spectral Density $S(\hbar\omega)$ [eV$^{-1}$]")
|
|
172
|
+
else:
|
|
173
|
+
if emission:
|
|
174
|
+
ax.set_ylabel(r"Luminescence $L(\hbar\omega)$ [eV$^{-1}$]")
|
|
175
|
+
else:
|
|
176
|
+
ax.set_ylabel(r"Absorption $L(\hbar\omega)$ [eV$^{-1}$]")
|
|
177
|
+
return fig, ax
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def plot_spec_funcs(
|
|
181
|
+
dynmats: tuple | np.ndarray | Path | str | list[tuple | np.ndarray | Path | str],
|
|
182
|
+
dq: np.ndarray | None,
|
|
183
|
+
dE: float,
|
|
184
|
+
gamma_zpl: float = 0.001,
|
|
185
|
+
sigma_zpl: float = 0.0,
|
|
186
|
+
sigma_psb: tuple[float, float] = (0.005, 0.001),
|
|
187
|
+
gamma_psb: tuple[float, float] | None = None,
|
|
188
|
+
emission: bool = True,
|
|
189
|
+
omega_mult: float = 5.0,
|
|
190
|
+
omega_max: float = 0.0,
|
|
191
|
+
norm: str = "area",
|
|
192
|
+
T: float = 0,
|
|
193
|
+
plot_type: str = "subplot",
|
|
194
|
+
figsize: tuple[float, float] = (8.0, 2.5),
|
|
195
|
+
skip_lim_adjust: bool = False,
|
|
196
|
+
ax=None,
|
|
197
|
+
):
|
|
198
|
+
"""Make a plot of the spectral functions and luminescence/absorption intensity.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
dynmats (tuple | np.ndarray | Path | str): path to a dynamical matrix in .npz format or a
|
|
202
|
+
np.ndarray corresponding to a dynamical matrix (shape 3N x 3N). If a tuple is given,
|
|
203
|
+
assume that the spectral functions have already been obtained. The tuple should contain
|
|
204
|
+
elements (w, dos, S, w_L, L) where w is the freq grid of dos/S and w_L is the freq grid
|
|
205
|
+
of L. A list of dynmats can be provided instead.
|
|
206
|
+
dq (np.ndarray): mass-weighted displacement vector in (amu^{1/2} Ang). Can be None if all
|
|
207
|
+
dynmats that are provided are of type tuple (see above).
|
|
208
|
+
dE (float): energy of the zero-phonon line.
|
|
209
|
+
gamma_zpl (float): Lorentzian broadening in the ZPL to capture homogeneous broadening.
|
|
210
|
+
sigma_zpl (float): Gaussian broadening in the ZPL to capture inhomogeneous broadening.
|
|
211
|
+
sigma_psb (float, float): Gaussian broadening used to broaden the partial Huang-Rhys
|
|
212
|
+
factors. The broadening factor is linearly interpolated from sigma_psb[0] at zero
|
|
213
|
+
frequency to sigma_psb[1] at the highest (non-LVM) frequency.
|
|
214
|
+
gamma_psb (float, float): Turns on Lorentzian broadening of local vibrational modes
|
|
215
|
+
identified by their inverse participation ratio. gamma_psb[0] is ipr_cut and
|
|
216
|
+
gamma_psb[1] is gamma_lvm. See :class:`Broadening`.
|
|
217
|
+
emission (bool): determines if the intensity corresponds to emission or absorption.
|
|
218
|
+
omega_mult (float): how many factors of the maximum phonon frequency from the ZPL will be
|
|
219
|
+
plotted in the luminescence/absorption intensity.
|
|
220
|
+
omega_max (float): maximum phonon frequency can be provided if known, used in determining
|
|
221
|
+
the plot ranges.
|
|
222
|
+
norm (str): normalization of luminescence (area or max).
|
|
223
|
+
T (float): Temperature in kelvin.
|
|
224
|
+
plot_type (str): type of plot to generate. "subplot" will generate a subplot for the dos, S
|
|
225
|
+
and L, respectively. "inset" will plot L with S as an inset. To make a plot of a single
|
|
226
|
+
type of function, specify "dos", "S", or "L".
|
|
227
|
+
figsize (tuple): figsize passed to `matplotlib.pyplot.subplots`.
|
|
228
|
+
skip_lim_adjust (bool): do not adjust xlim and ylim when making plots.
|
|
229
|
+
ax (plt.Axes): matplotlib axes object (an array of them) if already made.
|
|
230
|
+
"""
|
|
231
|
+
if not isinstance(dynmats, list):
|
|
232
|
+
dynmats = [dynmats]
|
|
233
|
+
|
|
234
|
+
spec_funcs = []
|
|
235
|
+
for i, dynmat in enumerate(dynmats):
|
|
236
|
+
if isinstance(dynmat, tuple):
|
|
237
|
+
spec_funcs.append(dynmat)
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
logger.info(f"dynmat {i} - diagonalizing")
|
|
241
|
+
omega, U = get_phonons(dynmat)
|
|
242
|
+
dq_k = U.T @ dq
|
|
243
|
+
|
|
244
|
+
if gamma_psb is not None:
|
|
245
|
+
logger.info(f"dynmat {i} - computing inverse participation ratios")
|
|
246
|
+
ipr, ipr_cut, gamma_lvm = get_ipr(U), gamma_psb[0], gamma_psb[1]
|
|
247
|
+
else:
|
|
248
|
+
ipr, ipr_cut, gamma_lvm = None, None, None
|
|
249
|
+
|
|
250
|
+
if omega2eV * omega.max() > omega_max:
|
|
251
|
+
omega_max = omega2eV * omega.max()
|
|
252
|
+
|
|
253
|
+
logger.info(f"dynmat{i} - evaluating spectral functions")
|
|
254
|
+
w, dos, S, A = get_phonon_spec_func(
|
|
255
|
+
dq_k,
|
|
256
|
+
omega,
|
|
257
|
+
sigma_psb=sigma_psb,
|
|
258
|
+
gamma_zpl=gamma_zpl,
|
|
259
|
+
sigma_zpl=sigma_zpl,
|
|
260
|
+
ipr=ipr,
|
|
261
|
+
ipr_cut=ipr_cut,
|
|
262
|
+
gamma_lvm=gamma_lvm,
|
|
263
|
+
T=T,
|
|
264
|
+
)
|
|
265
|
+
tw, L = convert_A_to_L(w, A, dE, emission=emission, norm=norm)
|
|
266
|
+
|
|
267
|
+
spec_funcs.append((w, dos, S, tw, L))
|
|
268
|
+
|
|
269
|
+
if omega_max <= 0.0 and not skip_lim_adjust:
|
|
270
|
+
logger.warning("the maximum omega could not be determined, falling back to 0.1")
|
|
271
|
+
omega_max = 0.1
|
|
272
|
+
|
|
273
|
+
if plot_type[0].lower() == "i":
|
|
274
|
+
return _make_inset(
|
|
275
|
+
spec_funcs, dE, emission, omega_max, omega_mult, figsize, skip_lim_adjust, ax=ax
|
|
276
|
+
)
|
|
277
|
+
elif plot_type.lower()[:2] == "su":
|
|
278
|
+
return _make_subplots(
|
|
279
|
+
spec_funcs, dE, emission, omega_max, omega_mult, figsize, skip_lim_adjust, ax=ax
|
|
280
|
+
)
|
|
281
|
+
elif plot_type[0].lower() in ("d", "s", "l"):
|
|
282
|
+
return _make_single(
|
|
283
|
+
plot_type,
|
|
284
|
+
spec_funcs,
|
|
285
|
+
dE,
|
|
286
|
+
emission,
|
|
287
|
+
omega_max,
|
|
288
|
+
omega_mult,
|
|
289
|
+
figsize,
|
|
290
|
+
skip_lim_adjust,
|
|
291
|
+
ax=ax,
|
|
292
|
+
)
|
|
293
|
+
else:
|
|
294
|
+
raise ValueError(f"unknown plot_type {plot_type}")
|