Skip to content

defectpl.ks_analysis

extract_ksplot_data

extract_ksplot_data(
    eigenval_data, vbm, cbm, espan=1.0, sep=0.1, lim=10.0
)

Process raw k-point eigenvalue metrics to build a complete plotting data model.

Extracts windows, maps degeneracy spacing matrices, updates offsets, and returns a serialized data model container.

Parameters:

Name Type Description Default
eigenval_data dict

Raw unpacked dictionary generated via read_eigenval_file.

required
vbm float

Energy of the Valence Band Maximum (eV).

required
cbm float

Energy of the Conduction Band Minimum (eV).

required
espan float

Energy buffer width padding the VBM and CBM boundary fields (eV).

1.0
sep float

Lateral separation parameter adjusting near-degenerate points.

0.1
lim float

The maximum lateral geometric width limit.

10.0

Returns:

Type Description
KohnShamPlotData

A data container populated with processed levels and spatial coordinates.

Source code in defectpl\ks_analysis.py
def extract_ksplot_data(
    eigenval_data: Dict[str, Any],
    vbm: float,
    cbm: float,
    espan: float = 1.0,
    sep: float = 0.1,
    lim: float = 10.0,
) -> KohnShamPlotData:
    """
    Process raw k-point eigenvalue metrics to build a complete plotting data model.

    Extracts windows, maps degeneracy spacing matrices, updates offsets,
    and returns a serialized data model container.

    Parameters
    ----------
    eigenval_data : dict
        Raw unpacked dictionary generated via `read_eigenval_file`.
    vbm : float
        Energy of the Valence Band Maximum (eV).
    cbm : float
        Energy of the Conduction Band Minimum (eV).
    espan : float, default 1.0
        Energy buffer width padding the VBM and CBM boundary fields (eV).
    sep : float, default 0.1
        Lateral separation parameter adjusting near-degenerate points.
    lim : float, default 10.0
        The maximum lateral geometric width limit.

    Returns
    -------
    KohnShamPlotData
        A data container populated with processed levels and spatial coordinates.
    """
    emin = vbm - espan
    emax = cbm + espan

    up_trunc, up_idx = truncate_eigenval(eigenval_data["up"], emin, emax)
    down_trunc, down_idx = truncate_eigenval(eigenval_data["down"], emin, emax)

    up_energies, up_occupations = split_energy_occupation(up_trunc)
    down_energies, down_occupations = split_energy_occupation(down_trunc)

    degenerate_up = find_degenerate_eigenvalues(up_trunc)
    degenerate_down = find_degenerate_eigenvalues(down_trunc)

    max_div_up = max(len(g) for g in degenerate_up) if degenerate_up else 1
    max_div_down = max(len(g) for g in degenerate_down) if degenerate_down else 1

    xvalues_up = [-x for x in get_x_values(degenerate_up, max_div_up, sep, lim)]
    xvalues_down = get_x_values(degenerate_down, max_div_down, sep, lim)

    occupied_up, unoccupied_up = get_occupied_unoccupied_split(
        up_occupations, xvalues_up, up_energies
    )
    occupied_down, unoccupied_down = get_occupied_unoccupied_split(
        down_occupations, xvalues_down, down_energies
    )

    min_div = min(max_div_up, max_div_down)
    w = (lim - sep) / min_div - sep if min_div > 0 else (lim - sep)

    meta_info = {
        "selected_kpoint": eigenval_data.get("selected_kpoint"),
        "spin_multiplicity": eigenval_data.get("spin_multiplicity"),
        "nelect": eigenval_data.get("nelect"),
    }

    return KohnShamPlotData(
        up=up_trunc,
        down=down_trunc,
        up_idx=up_idx,
        down_idx=down_idx,
        up_energies=up_energies,
        up_occupations=up_occupations,
        down_energies=down_energies,
        down_occupations=down_occupations,
        degenerate_up=degenerate_up,
        degenerate_down=degenerate_down,
        max_div_up=max_div_up,
        max_div_down=max_div_down,
        xvalues_up=xvalues_up,
        xvalues_down=xvalues_down,
        occupied_up=occupied_up,
        unoccupied_up=unoccupied_up,
        occupied_down=occupied_down,
        unoccupied_down=unoccupied_down,
        vbm=vbm,
        cbm=cbm,
        emin=emin,
        emax=emax,
        espan=espan,
        sep=sep,
        lim=lim,
        w=w,
        meta_info=meta_info,
    )

plot_spin_resolved_levels

plot_spin_resolved_levels(
    data,
    output_filename="ks_plot.png",
    style_file=None,
    ax=None,
)

Plot Kohn-Sham energy levels with separate spin-up and spin-down panels.

Renders energy levels alongside shaded regions for the valence and conduction bands, and marks state occupancy. Supports native Matplotlib Axes injection.

Parameters:

Name Type Description Default
data KohnShamPlotData

The processed data container containing the state properties and layout coordinates.

required
output_filename str or Path

The path and file name where the final plot image will be saved if ax is None.

"ks_plot.png"
style_file str

An optional path to a matplotlib .mplstyle configuration file.

None
ax Axes

An existing Matplotlib Axes object to paint the plot on. If None, a new standalone figure is instantiated and written to output_filename.

None

Returns:

Type Description
Axes or None

Returns the active axes object if an ax argument was injected; otherwise saves the figure to disk and returns None.

Source code in defectpl\ks_analysis.py
def plot_spin_resolved_levels(
    data: KohnShamPlotData,
    output_filename: Union[str, Path] = "ks_plot.png",
    style_file: Optional[str] = None,
    ax: Optional[plt.Axes] = None,
) -> Optional[plt.Axes]:
    """
    Plot Kohn-Sham energy levels with separate spin-up and spin-down panels.

    Renders energy levels alongside shaded regions for the valence and conduction
    bands, and marks state occupancy. Supports native Matplotlib Axes injection.

    Parameters
    ----------
    data : KohnShamPlotData
        The processed data container containing the state properties and layout coordinates.
    output_filename : str or pathlib.Path, default "ks_plot.png"
        The path and file name where the final plot image will be saved if ax is None.
    style_file : str, optional
        An optional path to a matplotlib `.mplstyle` configuration file.
    ax : matplotlib.axes.Axes, optional
        An existing Matplotlib Axes object to paint the plot on. If None, a new
        standalone figure is instantiated and written to output_filename.

    Returns
    -------
    matplotlib.axes.Axes or None
        Returns the active axes object if an ax argument was injected; otherwise
        saves the figure to disk and returns None.
    """
    if style_file and os.path.exists(style_file):
        plt.style.use(style_file)
        if plt.rcParams.get("text.usetex") and shutil.which("latex") is None:
            plt.rcParams["text.usetex"] = False

    figsize = (6, 6)
    vbm_cbm_color = {"vbm": "orange", "cbm": "green", "alpha": 0.5}

    standalone = ax is None
    if standalone:
        fig, target_ax = plt.subplots(figsize=figsize)
    else:
        target_ax = ax

    target_ax.set_xlim(-data.lim, data.lim)
    target_ax.set_ylim(data.emin, data.emax)
    target_ax.axvline(0, color="black", linestyle="--", alpha=0.5)

    # Shade bulk band edges regions
    target_ax.axhspan(
        data.emin, data.vbm, color=vbm_cbm_color["vbm"], alpha=vbm_cbm_color["alpha"]
    )
    target_ax.axhspan(
        data.cbm, data.emax, color=vbm_cbm_color["cbm"], alpha=vbm_cbm_color["alpha"]
    )

    # Adjust marker line layout scaling dynamically
    s = data.w * 200 * (figsize[0] / 6.0)
    target_ax.scatter(data.xvalues_up, data.up_energies, color="black", marker="_", s=s)
    target_ax.scatter(
        data.xvalues_down, data.down_energies, color="black", marker="_", s=s
    )

    electron_markers = {"occupied": "o", "unoccupied": "x", "s": s / 25.0}

    # Draw electron occupation dots and holes representations
    target_ax.scatter(
        data.occupied_up["xvalues"],
        data.occupied_up["energies"],
        color="k",
        marker=electron_markers["occupied"],
        s=electron_markers["s"],
    )
    target_ax.scatter(
        data.unoccupied_up["xvalues"],
        data.unoccupied_up["energies"],
        color="k",
        marker=electron_markers["unoccupied"],
        s=electron_markers["s"],
    )
    target_ax.scatter(
        data.occupied_down["xvalues"],
        data.occupied_down["energies"],
        color="k",
        marker=electron_markers["occupied"],
        s=electron_markers["s"],
    )
    target_ax.scatter(
        data.unoccupied_down["xvalues"],
        data.unoccupied_down["energies"],
        color="k",
        marker=electron_markers["unoccupied"],
        s=electron_markers["s"],
    )

    target_ax.set_ylabel("Energy (eV)")
    target_ax.set_xticks([])
    target_ax.set_xticklabels([])

    if standalone:
        plt.savefig(Path(output_filename), dpi=300, bbox_inches="tight")
        plt.close()
        return None

    return target_ax

plot_ks_with_pr

plot_ks_with_pr(
    ks_data,
    pr_result,
    metric="p_ratio",
    cmap="RdYlGn_r",
    vmin=0.0,
    vmax=1.0,
    threshold=None,
    kpt_idx=0,
    title=None,
    output_filename="ks_pr_plot.png",
    figsize=(7, 6),
    dpi=300,
    style_file=None,
)

Plot Kohn-Sham energy levels colour-coded by participation ratio (P-ratio or IPR).

The layout follows :func:plot_spin_resolved_levels (spin-up on the left, spin-down on the right with a dividing vertical dashed line) but each horizontal level bar is coloured by the metric value fetched from pr_result instead of being drawn in uniform black.

Parameters:

Name Type Description Default
ks_data KohnShamPlotData

Processed eigenvalue data container (from :func:extract_ksplot_data).

required
pr_result dict

Nested participation-ratio result dict (loaded from participation_ratio.json or returned by :func:compute_participation_ratios).

required
metric (p_ratio, ipr)

The localization metric used for coloring. Default "p_ratio".

"p_ratio"
cmap str

Matplotlib colormap name. Default "RdYlGn_r" (green = low, red = high).

'RdYlGn_r'
vmin float

Colormap lower bound. Default 0.0.

0.0
vmax float

Colormap upper bound. Default 1.0.

1.0
threshold float

If provided, draw a horizontal dashed line at this metric value to guide the eye — only meaningful when a dual-axis view is used. (Currently the colour already encodes the metric, so this is optional.)

None
kpt_idx int

0-based k-point index to use when looking up PR values. Default 0.

0
title str

Figure title. Defaults to the defect name stored in pr_result.

None
output_filename str or Path

Destination file path. Default "ks_pr_plot.png".

'ks_pr_plot.png'
figsize tuple of float

Figure size (width, height) in inches.

(7, 6)
dpi int

Image resolution.

300
style_file str

Optional matplotlib .mplstyle file path.

None

Returns:

Type Description
None

Saves the figure to output_filename.

Source code in defectpl\ks_analysis.py
def plot_ks_with_pr(
    ks_data: KohnShamPlotData,
    pr_result: dict,
    metric: str = "p_ratio",
    cmap: str = "RdYlGn_r",
    vmin: float = 0.0,
    vmax: float = 1.0,
    threshold: Optional[float] = None,
    kpt_idx: int = 0,
    title: Optional[str] = None,
    output_filename: Union[str, Path] = "ks_pr_plot.png",
    figsize: Tuple[float, float] = (7, 6),
    dpi: int = 300,
    style_file: Optional[str] = None,
) -> None:
    """
    Plot Kohn-Sham energy levels colour-coded by participation ratio (P-ratio or IPR).

    The layout follows :func:`plot_spin_resolved_levels` (spin-up on the left,
    spin-down on the right with a dividing vertical dashed line) but each
    horizontal level bar is coloured by the *metric* value fetched from
    *pr_result* instead of being drawn in uniform black.

    Parameters
    ----------
    ks_data : KohnShamPlotData
        Processed eigenvalue data container (from :func:`extract_ksplot_data`).
    pr_result : dict
        Nested participation-ratio result dict (loaded from
        ``participation_ratio.json`` or returned by
        :func:`compute_participation_ratios`).
    metric : {"p_ratio", "ipr"}
        The localization metric used for coloring.  Default ``"p_ratio"``.
    cmap : str
        Matplotlib colormap name.  Default ``"RdYlGn_r"`` (green = low, red = high).
    vmin : float
        Colormap lower bound.  Default 0.0.
    vmax : float
        Colormap upper bound.  Default 1.0.
    threshold : float, optional
        If provided, draw a horizontal dashed line at this metric value to
        guide the eye — only meaningful when a dual-axis view is used.
        (Currently the colour already encodes the metric, so this is optional.)
    kpt_idx : int
        0-based k-point index to use when looking up PR values.  Default 0.
    title : str, optional
        Figure title.  Defaults to the defect name stored in *pr_result*.
    output_filename : str or Path
        Destination file path.  Default ``"ks_pr_plot.png"``.
    figsize : tuple of float
        Figure size (width, height) in inches.
    dpi : int
        Image resolution.
    style_file : str, optional
        Optional matplotlib ``.mplstyle`` file path.

    Returns
    -------
    None
        Saves the figure to *output_filename*.
    """
    if style_file and os.path.exists(style_file):
        plt.style.use(style_file)
        if plt.rcParams.get("text.usetex") and shutil.which("latex") is None:
            plt.rcParams["text.usetex"] = False

    import matplotlib.cm as cm
    import matplotlib.colors as mcolors

    norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
    import matplotlib as _mpl

    try:
        colormap = _mpl.colormaps[cmap]
    except AttributeError:
        colormap = cm.get_cmap(cmap)  # matplotlib < 3.5 fallback
    scalar_map = cm.ScalarMappable(norm=norm, cmap=colormap)
    scalar_map.set_array([])

    defect_name = pr_result.get("defect_name", "defect")

    # ── figure layout ─────────────────────────────────────────────────────────
    fig, target_ax = plt.subplots(figsize=figsize)

    target_ax.set_xlim(-ks_data.lim, ks_data.lim)
    target_ax.set_ylim(ks_data.emin, ks_data.emax)
    target_ax.axvline(0, color="black", linestyle="--", alpha=0.5)

    vbm_cbm_color = {"vbm": "orange", "cbm": "green", "alpha": 0.35}
    target_ax.axhspan(
        ks_data.emin,
        ks_data.vbm,
        color=vbm_cbm_color["vbm"],
        alpha=vbm_cbm_color["alpha"],
    )
    target_ax.axhspan(
        ks_data.cbm,
        ks_data.emax,
        color=vbm_cbm_color["cbm"],
        alpha=vbm_cbm_color["alpha"],
    )

    s = ks_data.w * 200 * (figsize[0] / 6.0)

    # ── spin-up (left, spin_1) ────────────────────────────────────────────────
    up_pr = _lookup_pr_values(
        pr_result, ks_data.up_idx, "spin_1", metric, kpt_idx=kpt_idx
    )
    up_colors = [
        scalar_map.to_rgba(v) if not (v != v) else "lightgray"  # NaN → gray
        for v in up_pr
    ]
    target_ax.scatter(
        ks_data.xvalues_up,
        ks_data.up_energies,
        c=up_colors,
        marker="_",
        s=s,
        zorder=3,
        linewidths=2,
    )

    # ── spin-down (right, spin_2) ─────────────────────────────────────────────
    spin2_label = "spin_2" if "spin_2" in pr_result.get("data", {}) else "spin_1"
    down_pr = _lookup_pr_values(
        pr_result, ks_data.down_idx, spin2_label, metric, kpt_idx=kpt_idx
    )
    down_colors = [
        scalar_map.to_rgba(v) if not (v != v) else "lightgray" for v in down_pr
    ]
    target_ax.scatter(
        ks_data.xvalues_down,
        ks_data.down_energies,
        c=down_colors,
        marker="_",
        s=s,
        zorder=3,
        linewidths=2,
    )

    # ── occupation markers ────────────────────────────────────────────────────
    em = s / 25.0
    for xv, en, occ in zip(
        ks_data.xvalues_up, ks_data.up_energies, ks_data.up_occupations
    ):
        mk = "o" if occ > 0.6 else "x"
        target_ax.scatter([xv], [en], color="k", marker=mk, s=em, zorder=4)
    for xv, en, occ in zip(
        ks_data.xvalues_down, ks_data.down_energies, ks_data.down_occupations
    ):
        mk = "o" if occ > 0.6 else "x"
        target_ax.scatter([xv], [en], color="k", marker=mk, s=em, zorder=4)

    # ── labels & colorbar ─────────────────────────────────────────────────────
    metric_label = {"p_ratio": "P-ratio", "ipr": "IPR"}.get(metric, metric)
    cbar = fig.colorbar(scalar_map, ax=target_ax, pad=0.02, fraction=0.04)
    cbar.set_label(metric_label, fontsize=9)

    target_ax.set_ylabel("Energy (eV)")
    target_ax.set_xticks([])
    target_ax.set_xticklabels([])

    # Spin-channel labels below x-axis
    target_ax.text(
        -ks_data.lim / 2,
        ks_data.emin,
        "spin ↑",
        ha="center",
        va="bottom",
        fontsize=8,
        color="gray",
    )
    target_ax.text(
        ks_data.lim / 2,
        ks_data.emin,
        "spin ↓",
        ha="center",
        va="bottom",
        fontsize=8,
        color="gray",
    )

    target_ax.set_title(title or defect_name)

    fig.tight_layout()
    fig.savefig(Path(output_filename), dpi=dpi, bbox_inches="tight")
    plt.close(fig)

get_homo_lumo_idx

get_homo_lumo_idx(eigenval, thr=0.6)

Locate the Highest Occupied and Lowest Unoccupied Molecular Orbital boundaries.

Parameters:

Name Type Description Default
eigenval list of list of float

A list of [energy, occupancy] state arrays targeting a single spin channel.

required
thr float

The occupancy threshold demarcating occupied from empty electronic levels.

0.6

Returns:

Name Type Description
homo_idx int

The index coordinate marking the HOMO band horizon.

lumo_idx int

The index coordinate marking the LUMO band horizon.

Source code in defectpl\ks_analysis.py
def get_homo_lumo_idx(eigenval: List[List[float]], thr: float = 0.6) -> Tuple[int, int]:
    """
    Locate the Highest Occupied and Lowest Unoccupied Molecular Orbital boundaries.

    Parameters
    ----------
    eigenval : list of list of float
        A list of [energy, occupancy] state arrays targeting a single spin channel.
    thr : float, default 0.6
        The occupancy threshold demarcating occupied from empty electronic levels.

    Returns
    -------
    homo_idx : int
        The index coordinate marking the HOMO band horizon.
    lumo_idx : int
        The index coordinate marking the LUMO band horizon.
    """
    ev_array = np.array(eigenval)
    occupations = ev_array[:, 1]
    if len(set(occupations)) > 2:
        warnings.warn(
            f"Fractional occupancies detected inside dataset. Threshold: {thr} applied."
        )

    homo_idx = max(i for i, occ in enumerate(occupations) if occ > thr)
    lumo_idx = min(i for i, occ in enumerate(occupations) if occ < thr)

    return homo_idx, lumo_idx