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.
@@ -0,0 +1,4 @@
1
+ """Constants shared by various portions of the code."""
2
+
3
+ omega2eV = 0.06465415134095606
4
+ kelvin2eV = 8.617333262145179e-05
@@ -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
@@ -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}")