Source code for vibeqc.plot
"""Matplotlib plotters for periodic-system observables.
This module is a *thin* presentation layer on top of
:mod:`vibeqc.bands`. It imports matplotlib lazily so that vibe-qc itself
does not impose a hard matplotlib dependency — callers only pay the
import cost when they actually draw something.
All functions accept an optional ``ax`` (or ``axes``) so they compose
into existing figures, and return the matplotlib ``Figure`` so the
caller can save it.
"""
from __future__ import annotations
from typing import Optional, Tuple
import numpy as np
from .bands import BandStructure, DensityOfStates
__all__ = [
"band_structure_figure",
"dos_figure",
"bands_dos_figure",
]
_HARTREE_TO_EV = 27.211386245988
def _require_matplotlib():
try:
import matplotlib.pyplot as plt # noqa: F401
except ImportError as e: # pragma: no cover - import error
raise ImportError(
"vibeqc.plot requires matplotlib. Install with "
"`pip install matplotlib`."
) from e
import matplotlib.pyplot as plt
return plt
[docs]
def band_structure_figure(
bs: BandStructure,
*,
ax=None,
units: str = "eV",
shift_to_fermi: bool = True,
color: str = "tab:blue",
linewidth: float = 1.0,
title: Optional[str] = None,
):
"""Draw a band-structure plot.
Parameters
----------
bs
:class:`vibeqc.BandStructure` from :func:`vibeqc.band_structure`.
ax
Existing matplotlib axes; a new figure is made if ``None``.
units
``"eV"`` (default) or ``"Hartree"``.
shift_to_fermi
If True and ``bs.e_fermi`` is set, shift the y-axis so the
Fermi level sits at zero.
"""
plt = _require_matplotlib()
if ax is None:
fig, ax = plt.subplots(figsize=(5, 4))
else:
fig = ax.figure
if units == "eV":
scale = _HARTREE_TO_EV
ylabel = "Energy (eV)"
elif units in ("Hartree", "Ha", "hartree"):
scale = 1.0
ylabel = "Energy (Hartree)"
else:
raise ValueError(f"Unknown units: {units!r}")
e0 = bs.e_fermi if (shift_to_fermi and bs.e_fermi is not None) else 0.0
energies = (bs.energies - e0) * scale
x = bs.kpath.distances
for n in range(bs.n_bands):
ax.plot(x, energies[:, n], color=color, lw=linewidth)
if shift_to_fermi and bs.e_fermi is not None:
ax.axhline(0.0, color="0.4", lw=0.6, ls="--")
ylabel = ylabel.replace("Energy", "E − E_F")
# High-symmetry tick marks.
ax.set_xticks([d for d, _ in bs.kpath.labels])
ax.set_xticklabels([lbl for _, lbl in bs.kpath.labels])
for d, _ in bs.kpath.labels:
ax.axvline(d, color="0.7", lw=0.5)
ax.set_xlim(x.min(), x.max())
ax.set_ylabel(ylabel)
ax.set_xlabel("k-path")
if title:
ax.set_title(title)
return fig
[docs]
def dos_figure(
dos: DensityOfStates,
*,
ax=None,
units: str = "eV",
shift_to_fermi: bool = True,
orientation: str = "vertical",
color: str = "tab:orange",
fill: bool = True,
title: Optional[str] = None,
):
"""Draw a density-of-states plot.
``orientation="horizontal"`` puts energy on the y-axis (so the
figure can sit beside a band-structure panel).
"""
plt = _require_matplotlib()
if ax is None:
fig, ax = plt.subplots(figsize=(4, 4))
else:
fig = ax.figure
if units == "eV":
scale = _HARTREE_TO_EV
e_label = "Energy (eV)"
elif units in ("Hartree", "Ha", "hartree"):
scale = 1.0
e_label = "Energy (Hartree)"
else:
raise ValueError(f"Unknown units: {units!r}")
e0 = dos.e_fermi if (shift_to_fermi and dos.e_fermi is not None) else 0.0
e = (dos.energies - e0) * scale
d = dos.dos / scale # density per unit energy → rescale with units
if orientation == "vertical":
ax.plot(e, d, color=color, lw=1.0)
if fill:
ax.fill_between(e, 0.0, d, color=color, alpha=0.25)
ax.set_xlabel(("E − E_F " if (shift_to_fermi and dos.e_fermi is not None) else "") + e_label)
ax.set_ylabel("DOS (states / energy / cell)")
if shift_to_fermi and dos.e_fermi is not None:
ax.axvline(0.0, color="0.4", lw=0.6, ls="--")
elif orientation == "horizontal":
ax.plot(d, e, color=color, lw=1.0)
if fill:
ax.fill_betweenx(e, 0.0, d, color=color, alpha=0.25)
ax.set_ylabel(("E − E_F " if (shift_to_fermi and dos.e_fermi is not None) else "") + e_label)
ax.set_xlabel("DOS")
if shift_to_fermi and dos.e_fermi is not None:
ax.axhline(0.0, color="0.4", lw=0.6, ls="--")
else:
raise ValueError(f"orientation must be 'vertical' or 'horizontal', got {orientation!r}")
if title:
ax.set_title(title)
return fig
[docs]
def bands_dos_figure(
bs: BandStructure,
dos: DensityOfStates,
*,
units: str = "eV",
shift_to_fermi: bool = True,
width_ratio: Tuple[float, float] = (3.0, 1.0),
title: Optional[str] = None,
):
"""Combined band structure + DOS panel — the standard solid-state
layout (bands on the left with shared y-axis to a horizontal DOS on
the right).
"""
plt = _require_matplotlib()
fig, (ax_b, ax_d) = plt.subplots(
1, 2, sharey=True,
gridspec_kw={"width_ratios": list(width_ratio)},
figsize=(7, 4),
)
band_structure_figure(
bs, ax=ax_b, units=units, shift_to_fermi=shift_to_fermi,
)
dos_figure(
dos, ax=ax_d, units=units, shift_to_fermi=shift_to_fermi,
orientation="horizontal",
)
ax_d.set_ylabel("")
if title:
fig.suptitle(title)
fig.tight_layout()
return fig