diffpes 2026.3.1__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.
- diffpes/__init__.py +65 -0
- diffpes/inout/__init__.py +88 -0
- diffpes/inout/chgcar.py +459 -0
- diffpes/inout/doscar.py +248 -0
- diffpes/inout/eigenval.py +258 -0
- diffpes/inout/hdf5.py +818 -0
- diffpes/inout/helpers.py +295 -0
- diffpes/inout/kpoints.py +433 -0
- diffpes/inout/plotting.py +757 -0
- diffpes/inout/poscar.py +174 -0
- diffpes/inout/procar.py +331 -0
- diffpes/inout/py.typed +0 -0
- diffpes/maths/__init__.py +68 -0
- diffpes/maths/dipole.py +368 -0
- diffpes/maths/gaunt.py +496 -0
- diffpes/maths/spherical_harmonics.py +336 -0
- diffpes/py.typed +0 -0
- diffpes/radial/__init__.py +47 -0
- diffpes/radial/bessel.py +198 -0
- diffpes/radial/integrate.py +128 -0
- diffpes/radial/wavefunctions.py +335 -0
- diffpes/simul/__init__.py +146 -0
- diffpes/simul/broadening.py +258 -0
- diffpes/simul/crosssections.py +196 -0
- diffpes/simul/expanded.py +1021 -0
- diffpes/simul/forward.py +586 -0
- diffpes/simul/oam.py +112 -0
- diffpes/simul/polarization.py +443 -0
- diffpes/simul/py.typed +0 -0
- diffpes/simul/resolution.py +122 -0
- diffpes/simul/self_energy.py +142 -0
- diffpes/simul/spectrum.py +1116 -0
- diffpes/simul/workflow.py +424 -0
- diffpes/tightb/__init__.py +68 -0
- diffpes/tightb/diagonalize.py +340 -0
- diffpes/tightb/hamiltonian.py +286 -0
- diffpes/tightb/projections.py +134 -0
- diffpes/types/__init__.py +162 -0
- diffpes/types/aliases.py +52 -0
- diffpes/types/bands.py +1064 -0
- diffpes/types/dos.py +510 -0
- diffpes/types/geometry.py +306 -0
- diffpes/types/kpath.py +365 -0
- diffpes/types/params.py +496 -0
- diffpes/types/py.typed +0 -0
- diffpes/types/radial_params.py +482 -0
- diffpes/types/self_energy.py +271 -0
- diffpes/types/tb_model.py +531 -0
- diffpes/types/volumetric.py +608 -0
- diffpes/utils/__init__.py +30 -0
- diffpes/utils/math.py +255 -0
- diffpes/utils/py.typed +0 -0
- diffpes-2026.3.1.dist-info/METADATA +176 -0
- diffpes-2026.3.1.dist-info/RECORD +55 -0
- diffpes-2026.3.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
"""Plotting utilities for ARPES spectra.
|
|
2
|
+
|
|
3
|
+
Extended Summary
|
|
4
|
+
----------------
|
|
5
|
+
Provides Matplotlib helper functions that consume an
|
|
6
|
+
:class:`~diffpes.types.ArpesSpectrum` PyTree directly and render
|
|
7
|
+
publication-style ARPES intensity maps.
|
|
8
|
+
|
|
9
|
+
Routine Listings
|
|
10
|
+
----------------
|
|
11
|
+
:func:`apply_kpath_ticks`
|
|
12
|
+
Apply symmetry-point tick labels from KPathInfo.
|
|
13
|
+
:func:`plot_arpes_spectrum`
|
|
14
|
+
Plot an ARPES intensity map from an ArpesSpectrum.
|
|
15
|
+
:func:`plot_arpes_with_kpath`
|
|
16
|
+
Plot spectrum and annotate the k-axis with symmetry labels.
|
|
17
|
+
|
|
18
|
+
Notes
|
|
19
|
+
-----
|
|
20
|
+
These functions operate on host-side NumPy arrays and Matplotlib
|
|
21
|
+
objects (not JAX-traced arrays). They are intended for visualization
|
|
22
|
+
at analysis time, not for inclusion inside ``jax.jit``-compiled
|
|
23
|
+
functions.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
from beartype import beartype
|
|
28
|
+
from beartype.typing import Literal, Optional, Tuple, Union
|
|
29
|
+
from matplotlib import pyplot as plt
|
|
30
|
+
from matplotlib.axes import Axes
|
|
31
|
+
from matplotlib.collections import PathCollection
|
|
32
|
+
from matplotlib.figure import Figure, SubFigure
|
|
33
|
+
from matplotlib.image import AxesImage
|
|
34
|
+
|
|
35
|
+
from diffpes.types import (
|
|
36
|
+
ArpesSpectrum,
|
|
37
|
+
BandStructure,
|
|
38
|
+
KPathInfo,
|
|
39
|
+
OrbitalProjection,
|
|
40
|
+
SpinOrbitalProjection,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
_INTENSITY_NDIM: int = 2
|
|
44
|
+
_ENERGY_AXIS_NDIM: int = 1
|
|
45
|
+
_BAND_NDIM: int = 2
|
|
46
|
+
|
|
47
|
+
_ORBITAL_INDEX: dict[str, int] = {
|
|
48
|
+
"s": 0,
|
|
49
|
+
"py": 1,
|
|
50
|
+
"pz": 2,
|
|
51
|
+
"px": 3,
|
|
52
|
+
"dxy": 4,
|
|
53
|
+
"dyz": 5,
|
|
54
|
+
"dz2": 6,
|
|
55
|
+
"dxz": 7,
|
|
56
|
+
"dx2y2": 8,
|
|
57
|
+
}
|
|
58
|
+
_PRESET_NAMES: tuple[str, ...] = (
|
|
59
|
+
"s",
|
|
60
|
+
"py",
|
|
61
|
+
"pz",
|
|
62
|
+
"px",
|
|
63
|
+
"p",
|
|
64
|
+
"dxy",
|
|
65
|
+
"dyz",
|
|
66
|
+
"dz2",
|
|
67
|
+
"dxz",
|
|
68
|
+
"dx2y2",
|
|
69
|
+
"d",
|
|
70
|
+
"non_s",
|
|
71
|
+
"total",
|
|
72
|
+
"spin_x_up",
|
|
73
|
+
"spin_x_down",
|
|
74
|
+
"spin_y_up",
|
|
75
|
+
"spin_y_down",
|
|
76
|
+
"spin_z_up",
|
|
77
|
+
"spin_z_down",
|
|
78
|
+
"spin_x",
|
|
79
|
+
"spin_y",
|
|
80
|
+
"spin_z",
|
|
81
|
+
"oam_p",
|
|
82
|
+
"oam_d",
|
|
83
|
+
"oam_total",
|
|
84
|
+
"oam_abs_total",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@beartype
|
|
89
|
+
def _prepare_plot_arrays(
|
|
90
|
+
spectrum: ArpesSpectrum,
|
|
91
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
92
|
+
"""Convert and validate spectrum arrays for plotting.
|
|
93
|
+
|
|
94
|
+
Extended Summary
|
|
95
|
+
----------------
|
|
96
|
+
Internal helper that normalizes an :class:`ArpesSpectrum` PyTree
|
|
97
|
+
into plain NumPy arrays and verifies that array ranks and lengths
|
|
98
|
+
are consistent with a 2D ARPES image.
|
|
99
|
+
|
|
100
|
+
Implementation Logic
|
|
101
|
+
--------------------
|
|
102
|
+
1. Convert ``spectrum.intensity`` and ``spectrum.energy_axis`` to
|
|
103
|
+
``np.float64`` arrays using ``np.asarray``.
|
|
104
|
+
2. Validate dimensions:
|
|
105
|
+
``intensity.ndim == 2`` and ``energy_axis.ndim == 1``.
|
|
106
|
+
3. Validate shape compatibility:
|
|
107
|
+
``intensity.shape[1] == energy_axis.shape[0]``.
|
|
108
|
+
4. Return normalized arrays for downstream plotting.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
spectrum : ArpesSpectrum
|
|
113
|
+
Input spectrum containing ``intensity`` and ``energy_axis``.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
intensity : np.ndarray
|
|
118
|
+
2D intensity array of shape ``(K, E)``.
|
|
119
|
+
energy_axis : np.ndarray
|
|
120
|
+
1D energy axis array of shape ``(E,)``.
|
|
121
|
+
|
|
122
|
+
Raises
|
|
123
|
+
------
|
|
124
|
+
ValueError
|
|
125
|
+
If array ranks are invalid or if intensity/energy sizes are
|
|
126
|
+
incompatible.
|
|
127
|
+
"""
|
|
128
|
+
intensity: np.ndarray = np.asarray(spectrum.intensity, dtype=np.float64)
|
|
129
|
+
energy_axis: np.ndarray = np.asarray(
|
|
130
|
+
spectrum.energy_axis, dtype=np.float64
|
|
131
|
+
)
|
|
132
|
+
if intensity.ndim != _INTENSITY_NDIM:
|
|
133
|
+
msg: str = "Expected spectrum.intensity to have shape (K, E)."
|
|
134
|
+
raise ValueError(msg)
|
|
135
|
+
if energy_axis.ndim != _ENERGY_AXIS_NDIM:
|
|
136
|
+
msg = "Expected spectrum.energy_axis to have shape (E,)."
|
|
137
|
+
raise ValueError(msg)
|
|
138
|
+
if intensity.shape[1] != energy_axis.shape[0]:
|
|
139
|
+
msg = (
|
|
140
|
+
"Incompatible shapes: intensity.shape[1] must equal "
|
|
141
|
+
"energy_axis.shape[0]."
|
|
142
|
+
)
|
|
143
|
+
raise ValueError(msg)
|
|
144
|
+
return intensity, energy_axis
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@beartype
|
|
148
|
+
def plot_arpes_spectrum(
|
|
149
|
+
spectrum: ArpesSpectrum,
|
|
150
|
+
ax: Optional[Axes] = None,
|
|
151
|
+
cmap: str = "gray",
|
|
152
|
+
colorbar: bool = True,
|
|
153
|
+
clim: Optional[tuple[float, float]] = None,
|
|
154
|
+
interpolation: str = "nearest",
|
|
155
|
+
aspect: Literal["equal", "auto"] = "auto",
|
|
156
|
+
xlabel: str = "k-point index",
|
|
157
|
+
ylabel: str = "Energy (eV)",
|
|
158
|
+
title: str = "Simulated ARPES Spectrum",
|
|
159
|
+
) -> Tuple[Union[Figure, SubFigure], Axes, AxesImage]:
|
|
160
|
+
"""Plot an ARPES intensity map from an ArpesSpectrum PyTree.
|
|
161
|
+
|
|
162
|
+
Extended Summary
|
|
163
|
+
----------------
|
|
164
|
+
Renders a 2D ARPES map using ``matplotlib.axes.Axes.imshow`` with
|
|
165
|
+
energy on the vertical axis and k-point index on the horizontal
|
|
166
|
+
axis. The function accepts an existing axis or creates a new figure
|
|
167
|
+
and axis when none is supplied.
|
|
168
|
+
|
|
169
|
+
Implementation Logic
|
|
170
|
+
--------------------
|
|
171
|
+
1. Normalize and validate arrays via :func:`_prepare_plot_arrays`.
|
|
172
|
+
2. Create a new figure/axis pair if ``ax`` is ``None``; otherwise
|
|
173
|
+
reuse the provided axis and its parent figure.
|
|
174
|
+
3. Compute plotting bounds from data:
|
|
175
|
+
x-range ``[0, K-1]`` and y-range ``[min(E), max(E)]``.
|
|
176
|
+
4. Draw ``intensity.T`` with ``origin="lower"`` so lower energies
|
|
177
|
+
appear at the bottom and k-index increases left to right.
|
|
178
|
+
5. Optionally apply color limits and add a labeled colorbar.
|
|
179
|
+
6. Set axis labels and title, then return figure, axis, and image.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
spectrum : ArpesSpectrum
|
|
184
|
+
Spectrum containing ``intensity`` of shape ``(K, E)`` and
|
|
185
|
+
``energy_axis`` of shape ``(E,)``.
|
|
186
|
+
ax : Optional[Axes], optional
|
|
187
|
+
Existing axis to draw on. If None, a new figure/axis is created.
|
|
188
|
+
cmap : str, optional
|
|
189
|
+
Matplotlib colormap name. Default is ``"gray"``.
|
|
190
|
+
colorbar : bool, optional
|
|
191
|
+
If True, add a colorbar labeled ``"Intensity (a.u.)"``.
|
|
192
|
+
clim : Optional[tuple[float, float]], optional
|
|
193
|
+
Optional ``(vmin, vmax)`` color limits.
|
|
194
|
+
interpolation : str, optional
|
|
195
|
+
Image interpolation mode. Default is ``"nearest"``.
|
|
196
|
+
aspect : str, optional
|
|
197
|
+
Image aspect ratio passed to ``imshow``. Default is ``"auto"``.
|
|
198
|
+
xlabel : str, optional
|
|
199
|
+
x-axis label text. Default is ``"k-point index"``.
|
|
200
|
+
ylabel : str, optional
|
|
201
|
+
y-axis label text. Default is ``"Energy (eV)"``.
|
|
202
|
+
title : str, optional
|
|
203
|
+
Axis title text. Default is ``"Simulated ARPES Spectrum"``.
|
|
204
|
+
|
|
205
|
+
Returns
|
|
206
|
+
-------
|
|
207
|
+
fig : Figure
|
|
208
|
+
Matplotlib figure object.
|
|
209
|
+
ax : Axes
|
|
210
|
+
Axis used for plotting.
|
|
211
|
+
image : AxesImage
|
|
212
|
+
Image artist created by ``imshow``.
|
|
213
|
+
|
|
214
|
+
Notes
|
|
215
|
+
-----
|
|
216
|
+
The transpose ``intensity.T`` is intentional:
|
|
217
|
+
input data is ``(K, E)``, while ``imshow`` expects rows to map to
|
|
218
|
+
the y-axis. Transposition maps energy to y and k-index to x.
|
|
219
|
+
"""
|
|
220
|
+
intensity: np.ndarray
|
|
221
|
+
energy_axis: np.ndarray
|
|
222
|
+
intensity, energy_axis = _prepare_plot_arrays(spectrum)
|
|
223
|
+
|
|
224
|
+
fig: Union[Figure, SubFigure]
|
|
225
|
+
if ax is None:
|
|
226
|
+
fig, ax = plt.subplots()
|
|
227
|
+
else:
|
|
228
|
+
fig = ax.figure
|
|
229
|
+
|
|
230
|
+
nkpoints: int = intensity.shape[0]
|
|
231
|
+
x_max: float = float(max(nkpoints - 1, 0))
|
|
232
|
+
e_min: float = float(np.min(energy_axis))
|
|
233
|
+
e_max: float = float(np.max(energy_axis))
|
|
234
|
+
|
|
235
|
+
image: AxesImage = ax.imshow(
|
|
236
|
+
intensity.T,
|
|
237
|
+
origin="lower",
|
|
238
|
+
aspect=aspect,
|
|
239
|
+
cmap=cmap,
|
|
240
|
+
interpolation=interpolation,
|
|
241
|
+
extent=(0.0, x_max, e_min, e_max),
|
|
242
|
+
)
|
|
243
|
+
if clim is not None:
|
|
244
|
+
image.set_clim(clim[0], clim[1])
|
|
245
|
+
if colorbar:
|
|
246
|
+
fig.colorbar(image, ax=ax, label="Intensity (a.u.)")
|
|
247
|
+
|
|
248
|
+
ax.set_xlabel(xlabel)
|
|
249
|
+
ax.set_ylabel(ylabel)
|
|
250
|
+
ax.set_title(title)
|
|
251
|
+
return fig, ax, image
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@beartype
|
|
255
|
+
def apply_kpath_ticks(
|
|
256
|
+
ax: Axes,
|
|
257
|
+
kpath: KPathInfo,
|
|
258
|
+
draw_symmetry_lines: bool = True,
|
|
259
|
+
line_color: str = "white",
|
|
260
|
+
line_width: float = 0.5,
|
|
261
|
+
line_alpha: float = 0.35,
|
|
262
|
+
) -> Axes:
|
|
263
|
+
"""Apply symmetry-point ticks/labels from KPathInfo to an axis.
|
|
264
|
+
|
|
265
|
+
Extended Summary
|
|
266
|
+
----------------
|
|
267
|
+
Adds k-path symmetry labels (e.g., G, M, K) to an existing axis
|
|
268
|
+
using :class:`KPathInfo`. Optionally draws vertical guide lines at
|
|
269
|
+
interior symmetry points to visually separate path segments.
|
|
270
|
+
|
|
271
|
+
Implementation Logic
|
|
272
|
+
--------------------
|
|
273
|
+
1. Convert ``kpath.label_indices`` to a Python list of ints.
|
|
274
|
+
2. Convert ``kpath.labels`` to a Python list of strings.
|
|
275
|
+
3. Truncate to the shorter list length to tolerate minor metadata
|
|
276
|
+
mismatches without raising.
|
|
277
|
+
4. Apply ticks and tick labels to the x-axis.
|
|
278
|
+
5. Optionally draw interior vertical lines at ticks excluding the
|
|
279
|
+
first and last symmetry points.
|
|
280
|
+
|
|
281
|
+
Parameters
|
|
282
|
+
----------
|
|
283
|
+
ax : Axes
|
|
284
|
+
Target axis.
|
|
285
|
+
kpath : KPathInfo
|
|
286
|
+
K-path metadata containing symmetry labels and their indices.
|
|
287
|
+
draw_symmetry_lines : bool, optional
|
|
288
|
+
If True, draw vertical guide lines at interior symmetry points.
|
|
289
|
+
line_color : str, optional
|
|
290
|
+
Color of symmetry guide lines.
|
|
291
|
+
line_width : float, optional
|
|
292
|
+
Width of symmetry guide lines.
|
|
293
|
+
line_alpha : float, optional
|
|
294
|
+
Alpha of symmetry guide lines.
|
|
295
|
+
|
|
296
|
+
Returns
|
|
297
|
+
-------
|
|
298
|
+
ax : Axes
|
|
299
|
+
The same axis, modified in place.
|
|
300
|
+
|
|
301
|
+
Notes
|
|
302
|
+
-----
|
|
303
|
+
This function mutates ``ax`` and returns it for convenient chaining.
|
|
304
|
+
"""
|
|
305
|
+
indices: list[int] = np.asarray(
|
|
306
|
+
kpath.label_indices, dtype=np.int32
|
|
307
|
+
).tolist()
|
|
308
|
+
labels: list[str] = list(kpath.labels)
|
|
309
|
+
n_labels: int = min(len(indices), len(labels))
|
|
310
|
+
if n_labels == 0:
|
|
311
|
+
return ax
|
|
312
|
+
|
|
313
|
+
ticks: list[float] = [float(idx) for idx in indices[:n_labels]]
|
|
314
|
+
ax.set_xticks(ticks)
|
|
315
|
+
ax.set_xticklabels(labels[:n_labels])
|
|
316
|
+
|
|
317
|
+
if draw_symmetry_lines:
|
|
318
|
+
for tick in ticks[1:-1]:
|
|
319
|
+
ax.axvline(
|
|
320
|
+
tick,
|
|
321
|
+
color=line_color,
|
|
322
|
+
linewidth=line_width,
|
|
323
|
+
alpha=line_alpha,
|
|
324
|
+
)
|
|
325
|
+
return ax
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@beartype
|
|
329
|
+
def plot_arpes_with_kpath( # noqa: PLR0913
|
|
330
|
+
spectrum: ArpesSpectrum,
|
|
331
|
+
kpath: KPathInfo,
|
|
332
|
+
ax: Optional[Axes] = None,
|
|
333
|
+
cmap: str = "gray",
|
|
334
|
+
colorbar: bool = True,
|
|
335
|
+
clim: Optional[tuple[float, float]] = None,
|
|
336
|
+
interpolation: str = "nearest",
|
|
337
|
+
aspect: Literal["equal", "auto"] = "auto",
|
|
338
|
+
xlabel: str = "Momentum (k)",
|
|
339
|
+
ylabel: str = "Energy (eV)",
|
|
340
|
+
title: str = "Simulated ARPES Spectrum",
|
|
341
|
+
draw_symmetry_lines: bool = True,
|
|
342
|
+
) -> Tuple[Union[Figure, SubFigure], Axes, AxesImage]:
|
|
343
|
+
"""Plot ARPES spectrum and annotate k-axis using KPathInfo.
|
|
344
|
+
|
|
345
|
+
Extended Summary
|
|
346
|
+
----------------
|
|
347
|
+
Convenience wrapper combining :func:`plot_arpes_spectrum` and
|
|
348
|
+
:func:`apply_kpath_ticks` in one call. Useful for line-mode band
|
|
349
|
+
paths where symmetry labels should be shown directly on the ARPES
|
|
350
|
+
image.
|
|
351
|
+
|
|
352
|
+
Implementation Logic
|
|
353
|
+
--------------------
|
|
354
|
+
1. Delegate base image rendering to :func:`plot_arpes_spectrum`
|
|
355
|
+
using the provided visual styling arguments.
|
|
356
|
+
2. Apply k-path ticks/labels (and optional symmetry guide lines)
|
|
357
|
+
via :func:`apply_kpath_ticks`.
|
|
358
|
+
3. Return the same figure/axis/image triple from the base plot.
|
|
359
|
+
|
|
360
|
+
Parameters
|
|
361
|
+
----------
|
|
362
|
+
spectrum : ArpesSpectrum
|
|
363
|
+
Spectrum to plot.
|
|
364
|
+
kpath : KPathInfo
|
|
365
|
+
Symmetry-point metadata used for x-axis annotation.
|
|
366
|
+
ax : Optional[Axes], optional
|
|
367
|
+
Existing axis to draw on. If None, create a new one.
|
|
368
|
+
cmap : str, optional
|
|
369
|
+
Matplotlib colormap name. Default is ``"gray"``.
|
|
370
|
+
colorbar : bool, optional
|
|
371
|
+
If True, add a colorbar.
|
|
372
|
+
clim : Optional[tuple[float, float]], optional
|
|
373
|
+
Optional ``(vmin, vmax)`` color limits.
|
|
374
|
+
interpolation : str, optional
|
|
375
|
+
Image interpolation mode for ``imshow``.
|
|
376
|
+
aspect : str, optional
|
|
377
|
+
Image aspect ratio for ``imshow``.
|
|
378
|
+
xlabel : str, optional
|
|
379
|
+
x-axis label. Default is ``"Momentum (k)"``.
|
|
380
|
+
ylabel : str, optional
|
|
381
|
+
y-axis label. Default is ``"Energy (eV)"``.
|
|
382
|
+
title : str, optional
|
|
383
|
+
Plot title.
|
|
384
|
+
draw_symmetry_lines : bool, optional
|
|
385
|
+
If True, draw vertical guide lines at interior symmetry points.
|
|
386
|
+
|
|
387
|
+
Returns
|
|
388
|
+
-------
|
|
389
|
+
fig : Figure
|
|
390
|
+
Matplotlib figure object.
|
|
391
|
+
ax : Axes
|
|
392
|
+
Axis used for plotting.
|
|
393
|
+
image : AxesImage
|
|
394
|
+
Image artist created by ``imshow``.
|
|
395
|
+
|
|
396
|
+
See Also
|
|
397
|
+
--------
|
|
398
|
+
plot_arpes_spectrum : Base ARPES heatmap renderer.
|
|
399
|
+
apply_kpath_ticks : K-path label and guide-line utility.
|
|
400
|
+
"""
|
|
401
|
+
fig: Union[Figure, SubFigure]
|
|
402
|
+
image: AxesImage
|
|
403
|
+
fig, ax, image = plot_arpes_spectrum(
|
|
404
|
+
spectrum=spectrum,
|
|
405
|
+
ax=ax,
|
|
406
|
+
cmap=cmap,
|
|
407
|
+
colorbar=colorbar,
|
|
408
|
+
clim=clim,
|
|
409
|
+
interpolation=interpolation,
|
|
410
|
+
aspect=aspect,
|
|
411
|
+
xlabel=xlabel,
|
|
412
|
+
ylabel=ylabel,
|
|
413
|
+
title=title,
|
|
414
|
+
)
|
|
415
|
+
apply_kpath_ticks(
|
|
416
|
+
ax=ax,
|
|
417
|
+
kpath=kpath,
|
|
418
|
+
draw_symmetry_lines=draw_symmetry_lines,
|
|
419
|
+
)
|
|
420
|
+
return fig, ax, image
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@beartype
|
|
424
|
+
def list_band_scatter_presets() -> tuple[str, ...]:
|
|
425
|
+
"""Return supported preset names for projected band scatter plots.
|
|
426
|
+
|
|
427
|
+
Returns
|
|
428
|
+
-------
|
|
429
|
+
tuple[str, ...]
|
|
430
|
+
Available preset names accepted by
|
|
431
|
+
:func:`plot_band_scatter_preset`.
|
|
432
|
+
"""
|
|
433
|
+
return _PRESET_NAMES
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@beartype
|
|
437
|
+
def _prepare_band_arrays(
|
|
438
|
+
bands: BandStructure,
|
|
439
|
+
) -> tuple[np.ndarray, float]:
|
|
440
|
+
"""Convert and validate band arrays for scatter plotting."""
|
|
441
|
+
eigenvalues: np.ndarray = np.asarray(bands.eigenvalues, dtype=np.float64)
|
|
442
|
+
if eigenvalues.ndim != _BAND_NDIM:
|
|
443
|
+
msg: str = "Expected bands.eigenvalues to have shape (K, B)."
|
|
444
|
+
raise ValueError(msg)
|
|
445
|
+
fermi: float = float(np.asarray(bands.fermi_energy, dtype=np.float64))
|
|
446
|
+
return eigenvalues, fermi
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@beartype
|
|
450
|
+
def _subset_atom_axis(
|
|
451
|
+
data: np.ndarray,
|
|
452
|
+
atom_indices: Optional[list[int]],
|
|
453
|
+
) -> np.ndarray:
|
|
454
|
+
"""Subset an array on atom axis (axis=2) when atom indices are provided."""
|
|
455
|
+
if atom_indices is None:
|
|
456
|
+
return data
|
|
457
|
+
idx: np.ndarray = np.asarray(atom_indices, dtype=np.int32)
|
|
458
|
+
return data[:, :, idx, :]
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@beartype
|
|
462
|
+
def _weights_from_preset( # noqa: PLR0912
|
|
463
|
+
orb_proj: Union[OrbitalProjection, SpinOrbitalProjection],
|
|
464
|
+
preset: str,
|
|
465
|
+
atom_indices: Optional[list[int]],
|
|
466
|
+
) -> tuple[np.ndarray, bool]:
|
|
467
|
+
"""Resolve a band-weight matrix from a preset name.
|
|
468
|
+
|
|
469
|
+
Returns
|
|
470
|
+
-------
|
|
471
|
+
weights : np.ndarray
|
|
472
|
+
Band weights with shape ``(K, B)``.
|
|
473
|
+
signed : bool
|
|
474
|
+
Whether weights are signed and should be color-mapped.
|
|
475
|
+
"""
|
|
476
|
+
key: str = preset.lower()
|
|
477
|
+
proj: np.ndarray = _subset_atom_axis(
|
|
478
|
+
np.asarray(orb_proj.projections, dtype=np.float64),
|
|
479
|
+
atom_indices,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
orbital_shells: dict[str, slice] = {
|
|
483
|
+
"p": slice(1, 4),
|
|
484
|
+
"d": slice(4, 9),
|
|
485
|
+
"non_s": slice(1, 9),
|
|
486
|
+
}
|
|
487
|
+
weights: Optional[np.ndarray] = None
|
|
488
|
+
signed: bool = False
|
|
489
|
+
|
|
490
|
+
if key in _ORBITAL_INDEX:
|
|
491
|
+
idx: int = _ORBITAL_INDEX[key]
|
|
492
|
+
weights = np.sum(proj[..., idx], axis=2)
|
|
493
|
+
elif key in orbital_shells:
|
|
494
|
+
weights = np.sum(proj[..., orbital_shells[key]], axis=(2, 3))
|
|
495
|
+
elif key == "total":
|
|
496
|
+
weights = np.sum(proj, axis=(2, 3))
|
|
497
|
+
|
|
498
|
+
spin_arr: Optional[np.ndarray] = None
|
|
499
|
+
if orb_proj.spin is not None:
|
|
500
|
+
spin_arr = _subset_atom_axis(
|
|
501
|
+
np.asarray(orb_proj.spin, dtype=np.float64),
|
|
502
|
+
atom_indices,
|
|
503
|
+
)
|
|
504
|
+
spin_channel: dict[str, int] = {
|
|
505
|
+
"spin_x_up": 0,
|
|
506
|
+
"spin_x_down": 1,
|
|
507
|
+
"spin_y_up": 2,
|
|
508
|
+
"spin_y_down": 3,
|
|
509
|
+
"spin_z_up": 4,
|
|
510
|
+
"spin_z_down": 5,
|
|
511
|
+
}
|
|
512
|
+
spin_net: dict[str, tuple[int, int]] = {
|
|
513
|
+
"spin_x": (0, 1),
|
|
514
|
+
"spin_y": (2, 3),
|
|
515
|
+
"spin_z": (4, 5),
|
|
516
|
+
}
|
|
517
|
+
if weights is None and key.startswith("spin_"):
|
|
518
|
+
if spin_arr is None:
|
|
519
|
+
msg = f"Preset '{preset}' requires spin data, but spin is None."
|
|
520
|
+
raise ValueError(msg)
|
|
521
|
+
if key in spin_channel:
|
|
522
|
+
weights = np.sum(spin_arr[..., spin_channel[key]], axis=2)
|
|
523
|
+
elif key in spin_net:
|
|
524
|
+
up_idx: int
|
|
525
|
+
dn_idx: int
|
|
526
|
+
up_idx, dn_idx = spin_net[key]
|
|
527
|
+
weights = np.sum(
|
|
528
|
+
spin_arr[..., up_idx] - spin_arr[..., dn_idx],
|
|
529
|
+
axis=2,
|
|
530
|
+
)
|
|
531
|
+
signed = True
|
|
532
|
+
|
|
533
|
+
oam_arr: Optional[np.ndarray] = None
|
|
534
|
+
if orb_proj.oam is not None:
|
|
535
|
+
oam_arr = _subset_atom_axis(
|
|
536
|
+
np.asarray(orb_proj.oam, dtype=np.float64),
|
|
537
|
+
atom_indices,
|
|
538
|
+
)
|
|
539
|
+
oam_component: dict[str, int] = {
|
|
540
|
+
"oam_p": 0,
|
|
541
|
+
"oam_d": 1,
|
|
542
|
+
"oam_total": 2,
|
|
543
|
+
}
|
|
544
|
+
if weights is None and key.startswith("oam_"):
|
|
545
|
+
if oam_arr is None:
|
|
546
|
+
msg = f"Preset '{preset}' requires OAM data, but oam is None."
|
|
547
|
+
raise ValueError(msg)
|
|
548
|
+
if key in oam_component:
|
|
549
|
+
weights = np.sum(oam_arr[..., oam_component[key]], axis=2)
|
|
550
|
+
signed = True
|
|
551
|
+
elif key == "oam_abs_total":
|
|
552
|
+
weights = np.sum(np.abs(oam_arr[..., 2]), axis=2)
|
|
553
|
+
signed = False
|
|
554
|
+
|
|
555
|
+
if weights is None:
|
|
556
|
+
presets: str = ", ".join(_PRESET_NAMES)
|
|
557
|
+
msg = f"Unknown preset '{preset}'. Available presets: {presets}."
|
|
558
|
+
raise ValueError(msg)
|
|
559
|
+
return weights, signed
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
@beartype
|
|
563
|
+
def plot_band_scatter_preset( # noqa: PLR0913
|
|
564
|
+
bands: BandStructure,
|
|
565
|
+
orb_proj: Union[OrbitalProjection, SpinOrbitalProjection],
|
|
566
|
+
preset: str = "p",
|
|
567
|
+
atom_indices: Optional[list[int]] = None,
|
|
568
|
+
ax: Optional[Axes] = None,
|
|
569
|
+
shift_fermi: bool = True,
|
|
570
|
+
size_scale: float = 250.0,
|
|
571
|
+
min_size: float = 0.5,
|
|
572
|
+
alpha: float = 0.75,
|
|
573
|
+
color: str = "tab:blue",
|
|
574
|
+
cmap: str = "coolwarm",
|
|
575
|
+
colorbar: bool = False,
|
|
576
|
+
xlabel: str = "Momentum (k)",
|
|
577
|
+
ylabel: str = "Energy (eV)",
|
|
578
|
+
title: str = "Projected Band Scatter",
|
|
579
|
+
) -> tuple[Union[Figure, SubFigure], Axes, PathCollection]:
|
|
580
|
+
"""Plot projected bands as marker-size-weighted scatter points.
|
|
581
|
+
|
|
582
|
+
Extended Summary
|
|
583
|
+
----------------
|
|
584
|
+
Builds a fat-band style scatter plot from a named preset. Marker
|
|
585
|
+
size encodes projection magnitude. For signed presets (spin net
|
|
586
|
+
components and OAM channels), marker color encodes sign/value via a
|
|
587
|
+
colormap.
|
|
588
|
+
|
|
589
|
+
Parameters
|
|
590
|
+
----------
|
|
591
|
+
bands : BandStructure
|
|
592
|
+
Band structure with ``eigenvalues`` shape ``(K, B)``.
|
|
593
|
+
orb_proj : OrbitalProjection or SpinOrbitalProjection
|
|
594
|
+
Projection object containing orbital weights and optional spin/OAM.
|
|
595
|
+
preset : str, optional
|
|
596
|
+
Preset key from :func:`list_band_scatter_presets`.
|
|
597
|
+
atom_indices : Optional[list[int]], optional
|
|
598
|
+
Optional 0-based atom indices used before reduction.
|
|
599
|
+
ax : Optional[Axes], optional
|
|
600
|
+
Existing axis to draw on. If None, a new figure/axis is created.
|
|
601
|
+
shift_fermi : bool, optional
|
|
602
|
+
If True, plot ``E - E_F`` on y-axis.
|
|
603
|
+
size_scale : float, optional
|
|
604
|
+
Linear scale factor for marker sizes.
|
|
605
|
+
min_size : float, optional
|
|
606
|
+
Minimum marker size in points^2.
|
|
607
|
+
alpha : float, optional
|
|
608
|
+
Marker alpha.
|
|
609
|
+
color : str, optional
|
|
610
|
+
Solid marker color for non-signed presets.
|
|
611
|
+
cmap : str, optional
|
|
612
|
+
Colormap name used for signed presets.
|
|
613
|
+
colorbar : bool, optional
|
|
614
|
+
If True and preset is signed, draw a colorbar.
|
|
615
|
+
xlabel : str, optional
|
|
616
|
+
X-axis label.
|
|
617
|
+
ylabel : str, optional
|
|
618
|
+
Y-axis label.
|
|
619
|
+
title : str, optional
|
|
620
|
+
Plot title.
|
|
621
|
+
|
|
622
|
+
Returns
|
|
623
|
+
-------
|
|
624
|
+
fig : Figure or SubFigure
|
|
625
|
+
Parent figure.
|
|
626
|
+
ax : Axes
|
|
627
|
+
Axis used for plotting.
|
|
628
|
+
scatter : PathCollection
|
|
629
|
+
Scatter artist returned by Matplotlib.
|
|
630
|
+
"""
|
|
631
|
+
eigenvalues: np.ndarray
|
|
632
|
+
fermi: float
|
|
633
|
+
eigenvalues, fermi = _prepare_band_arrays(bands)
|
|
634
|
+
weights: np.ndarray
|
|
635
|
+
signed: bool
|
|
636
|
+
weights, signed = _weights_from_preset(orb_proj, preset, atom_indices)
|
|
637
|
+
if weights.shape != eigenvalues.shape:
|
|
638
|
+
msg = (
|
|
639
|
+
"Preset weights must have shape matching "
|
|
640
|
+
"bands.eigenvalues (K, B)."
|
|
641
|
+
)
|
|
642
|
+
raise ValueError(msg)
|
|
643
|
+
|
|
644
|
+
yvals: np.ndarray = eigenvalues - fermi if shift_fermi else eigenvalues
|
|
645
|
+
nkpoints: int = yvals.shape[0]
|
|
646
|
+
xvals: np.ndarray = np.broadcast_to(
|
|
647
|
+
np.arange(nkpoints, dtype=np.float64)[:, np.newaxis],
|
|
648
|
+
yvals.shape,
|
|
649
|
+
)
|
|
650
|
+
marker_sizes: np.ndarray = np.maximum(
|
|
651
|
+
np.abs(weights) * size_scale,
|
|
652
|
+
min_size,
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
fig: Union[Figure, SubFigure]
|
|
656
|
+
if ax is None:
|
|
657
|
+
fig, ax = plt.subplots()
|
|
658
|
+
else:
|
|
659
|
+
fig = ax.figure
|
|
660
|
+
|
|
661
|
+
scatter: PathCollection
|
|
662
|
+
if signed:
|
|
663
|
+
scatter = ax.scatter(
|
|
664
|
+
xvals.ravel(),
|
|
665
|
+
yvals.ravel(),
|
|
666
|
+
s=marker_sizes.ravel(),
|
|
667
|
+
c=weights.ravel(),
|
|
668
|
+
cmap=cmap,
|
|
669
|
+
alpha=alpha,
|
|
670
|
+
edgecolors="none",
|
|
671
|
+
)
|
|
672
|
+
if colorbar:
|
|
673
|
+
fig.colorbar(scatter, ax=ax, label=f"{preset} weight")
|
|
674
|
+
else:
|
|
675
|
+
scatter = ax.scatter(
|
|
676
|
+
xvals.ravel(),
|
|
677
|
+
yvals.ravel(),
|
|
678
|
+
s=marker_sizes.ravel(),
|
|
679
|
+
color=color,
|
|
680
|
+
alpha=alpha,
|
|
681
|
+
edgecolors="none",
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
ax.set_xlabel(xlabel)
|
|
685
|
+
ax.set_ylabel(ylabel)
|
|
686
|
+
ax.set_title(title)
|
|
687
|
+
return fig, ax, scatter
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
@beartype
|
|
691
|
+
def plot_band_scatter_with_kpath( # noqa: PLR0913
|
|
692
|
+
bands: BandStructure,
|
|
693
|
+
orb_proj: Union[OrbitalProjection, SpinOrbitalProjection],
|
|
694
|
+
kpath: KPathInfo,
|
|
695
|
+
preset: str = "p",
|
|
696
|
+
atom_indices: Optional[list[int]] = None,
|
|
697
|
+
ax: Optional[Axes] = None,
|
|
698
|
+
shift_fermi: bool = True,
|
|
699
|
+
size_scale: float = 250.0,
|
|
700
|
+
min_size: float = 0.5,
|
|
701
|
+
alpha: float = 0.75,
|
|
702
|
+
color: str = "tab:blue",
|
|
703
|
+
cmap: str = "coolwarm",
|
|
704
|
+
colorbar: bool = False,
|
|
705
|
+
xlabel: str = "Momentum (k)",
|
|
706
|
+
ylabel: str = "Energy (eV)",
|
|
707
|
+
title: str = "Projected Band Scatter",
|
|
708
|
+
draw_symmetry_lines: bool = True,
|
|
709
|
+
) -> tuple[Union[Figure, SubFigure], Axes, PathCollection]:
|
|
710
|
+
"""Plot projected band scatter and annotate x-axis with k-path labels.
|
|
711
|
+
|
|
712
|
+
Returns
|
|
713
|
+
-------
|
|
714
|
+
fig : Figure or SubFigure
|
|
715
|
+
Parent figure.
|
|
716
|
+
ax : Axes
|
|
717
|
+
Axis used for plotting.
|
|
718
|
+
scatter : PathCollection
|
|
719
|
+
Scatter artist returned by Matplotlib.
|
|
720
|
+
"""
|
|
721
|
+
fig: Union[Figure, SubFigure]
|
|
722
|
+
scatter: PathCollection
|
|
723
|
+
fig, ax, scatter = plot_band_scatter_preset(
|
|
724
|
+
bands=bands,
|
|
725
|
+
orb_proj=orb_proj,
|
|
726
|
+
preset=preset,
|
|
727
|
+
atom_indices=atom_indices,
|
|
728
|
+
ax=ax,
|
|
729
|
+
shift_fermi=shift_fermi,
|
|
730
|
+
size_scale=size_scale,
|
|
731
|
+
min_size=min_size,
|
|
732
|
+
alpha=alpha,
|
|
733
|
+
color=color,
|
|
734
|
+
cmap=cmap,
|
|
735
|
+
colorbar=colorbar,
|
|
736
|
+
xlabel=xlabel,
|
|
737
|
+
ylabel=ylabel,
|
|
738
|
+
title=title,
|
|
739
|
+
)
|
|
740
|
+
apply_kpath_ticks(
|
|
741
|
+
ax=ax,
|
|
742
|
+
kpath=kpath,
|
|
743
|
+
draw_symmetry_lines=draw_symmetry_lines,
|
|
744
|
+
line_color="black",
|
|
745
|
+
line_alpha=0.35,
|
|
746
|
+
)
|
|
747
|
+
return fig, ax, scatter
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
__all__: list[str] = [
|
|
751
|
+
"apply_kpath_ticks",
|
|
752
|
+
"list_band_scatter_presets",
|
|
753
|
+
"plot_band_scatter_preset",
|
|
754
|
+
"plot_band_scatter_with_kpath",
|
|
755
|
+
"plot_arpes_spectrum",
|
|
756
|
+
"plot_arpes_with_kpath",
|
|
757
|
+
]
|