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.
Files changed (55) hide show
  1. diffpes/__init__.py +65 -0
  2. diffpes/inout/__init__.py +88 -0
  3. diffpes/inout/chgcar.py +459 -0
  4. diffpes/inout/doscar.py +248 -0
  5. diffpes/inout/eigenval.py +258 -0
  6. diffpes/inout/hdf5.py +818 -0
  7. diffpes/inout/helpers.py +295 -0
  8. diffpes/inout/kpoints.py +433 -0
  9. diffpes/inout/plotting.py +757 -0
  10. diffpes/inout/poscar.py +174 -0
  11. diffpes/inout/procar.py +331 -0
  12. diffpes/inout/py.typed +0 -0
  13. diffpes/maths/__init__.py +68 -0
  14. diffpes/maths/dipole.py +368 -0
  15. diffpes/maths/gaunt.py +496 -0
  16. diffpes/maths/spherical_harmonics.py +336 -0
  17. diffpes/py.typed +0 -0
  18. diffpes/radial/__init__.py +47 -0
  19. diffpes/radial/bessel.py +198 -0
  20. diffpes/radial/integrate.py +128 -0
  21. diffpes/radial/wavefunctions.py +335 -0
  22. diffpes/simul/__init__.py +146 -0
  23. diffpes/simul/broadening.py +258 -0
  24. diffpes/simul/crosssections.py +196 -0
  25. diffpes/simul/expanded.py +1021 -0
  26. diffpes/simul/forward.py +586 -0
  27. diffpes/simul/oam.py +112 -0
  28. diffpes/simul/polarization.py +443 -0
  29. diffpes/simul/py.typed +0 -0
  30. diffpes/simul/resolution.py +122 -0
  31. diffpes/simul/self_energy.py +142 -0
  32. diffpes/simul/spectrum.py +1116 -0
  33. diffpes/simul/workflow.py +424 -0
  34. diffpes/tightb/__init__.py +68 -0
  35. diffpes/tightb/diagonalize.py +340 -0
  36. diffpes/tightb/hamiltonian.py +286 -0
  37. diffpes/tightb/projections.py +134 -0
  38. diffpes/types/__init__.py +162 -0
  39. diffpes/types/aliases.py +52 -0
  40. diffpes/types/bands.py +1064 -0
  41. diffpes/types/dos.py +510 -0
  42. diffpes/types/geometry.py +306 -0
  43. diffpes/types/kpath.py +365 -0
  44. diffpes/types/params.py +496 -0
  45. diffpes/types/py.typed +0 -0
  46. diffpes/types/radial_params.py +482 -0
  47. diffpes/types/self_energy.py +271 -0
  48. diffpes/types/tb_model.py +531 -0
  49. diffpes/types/volumetric.py +608 -0
  50. diffpes/utils/__init__.py +30 -0
  51. diffpes/utils/math.py +255 -0
  52. diffpes/utils/py.typed +0 -0
  53. diffpes-2026.3.1.dist-info/METADATA +176 -0
  54. diffpes-2026.3.1.dist-info/RECORD +55 -0
  55. 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
+ ]