From e3f2afe3905d0ce740778b1faf6a59e12f052b7d Mon Sep 17 00:00:00 2001 From: AsymmetryChou <181240085@smail.nju.edu.cn> Date: Sun, 13 Sep 2026 15:37:38 +0800 Subject: [PATCH 1/6] Feat: add band_edge.py --- dpnegf/utils/band_edge.py | 82 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 dpnegf/utils/band_edge.py diff --git a/dpnegf/utils/band_edge.py b/dpnegf/utils/band_edge.py new file mode 100644 index 0000000..6924fe0 --- /dev/null +++ b/dpnegf/utils/band_edge.py @@ -0,0 +1,82 @@ +from typing import Tuple, Optional + +import numpy as np + + +def calculate_band_edges( + eigenvalues: np.ndarray, + neutral_electrons: float, + spin_degeneracy: int, + occupation_tolerance: float = 1e-8, + gap_tolerance: float = 1e-6, +) -> Tuple[float, float]: + """Return the conduction- and valence-band edges ``(E_c, E_v)``. + + The band indices are fixed by the neutral electron count, independently of + the Fermi level or any finite-temperature smearing. ``eigenvalues`` must + have shape ``(n_kpoints, n_bands)``. + """ + + eigenvalues = np.asarray(eigenvalues) + if eigenvalues.ndim != 2: + raise ValueError( + "eigenvalues must have shape (n_kpoints, n_bands), " + f"got {eigenvalues.shape}." + ) + if eigenvalues.shape[0] == 0 or eigenvalues.shape[1] == 0: + raise ValueError("eigenvalues must contain at least one k-point and band.") + if not np.all(np.isfinite(eigenvalues)): + raise ValueError("eigenvalues contain non-finite values.") + if spin_degeneracy not in (1, 2): + raise ValueError( + f"spin_degeneracy must be 1 or 2, got {spin_degeneracy}." + ) + + occupied_bands_float = float(neutral_electrons) / spin_degeneracy + occupied_bands = int(round(occupied_bands_float)) + if not np.isclose( + occupied_bands_float, occupied_bands, rtol=0.0, atol=occupation_tolerance + ): + raise ValueError( + "The neutral electron count does not define an integer number of " + f"occupied bands: neutral_electrons={neutral_electrons}, " + f"spin_degeneracy={spin_degeneracy}, " + f"n_occupied={occupied_bands_float}." + ) + + n_bands = eigenvalues.shape[1] + if not 1 <= occupied_bands < n_bands: + raise ValueError( + "The occupied-band index is outside the available spectrum: " + f"n_occupied={occupied_bands}, n_bands={n_bands}." + ) + + sorted_eigenvalues = np.sort(eigenvalues, axis=1) + + e_v = float(np.max(sorted_eigenvalues[:, occupied_bands - 1])) + e_c = float(np.min(sorted_eigenvalues[:, occupied_bands])) + band_gap = e_c - e_v + + if band_gap <= gap_tolerance: + raise ValueError( + "The sampled spectrum does not have a band gap greater than " + f"gap_tolerance={gap_tolerance}: " + f"E_v={e_v}, E_c={e_c}, E_g={band_gap}." + ) + + return e_c, e_v + +def validate_fermi_in_band_gap( + e_fermi: float, + e_c: float, + e_v: float, + lead_name: Optional[str] = None, +) -> None: + """Raise an error if the Fermi level is not in the band gap.""" + + label = f" for lead '{lead_name}'" if lead_name else "" + if not (e_v < e_fermi < e_c): + raise ValueError( + f"The Fermi level is not in the band gap{label}: " + f"E_v={e_v}, E_c={e_c}, E_F={e_fermi}." + ) \ No newline at end of file From 11d4648c306125102d4d59e9edc8eff60281b0c8 Mon Sep 17 00:00:00 2001 From: AsymmetryChou <181240085@smail.nju.edu.cn> Date: Sun, 13 Sep 2026 16:43:32 +0800 Subject: [PATCH 2/6] Feat: add test_band_edge.py --- dpnegf/tests/test_band_edge.py | 281 +++++++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 dpnegf/tests/test_band_edge.py diff --git a/dpnegf/tests/test_band_edge.py b/dpnegf/tests/test_band_edge.py new file mode 100644 index 0000000..732ff2b --- /dev/null +++ b/dpnegf/tests/test_band_edge.py @@ -0,0 +1,281 @@ +import copy +import json +import logging +from pathlib import Path + +import numpy as np +import pytest +import torch +from dargs.dargs import Argument, ArgumentTypeError +from dptb.nn.build import build_model + +from dpnegf.runner.NEGF import NEGF +from dpnegf.utils.argcheck import normalize_run +from dpnegf.utils.band_edge import calculate_band_edges, validate_fermi_in_band_gap +from dpnegf.utils.elec_struc_cal import ElecStruCal + + +ROOT = Path(__file__).resolve().parents[2] +HBN_ROOT = ROOT / "examples" / "hBN" +HBN_CHECKPOINT = HBN_ROOT / "train" / "train_out" / "checkpoint" / "nnsk.ep3000.pth" +HBN_PRIMITIVE = HBN_ROOT / "train" / "data" / "POSCAR" +HBN_NEGF_STRUCTURE = HBN_ROOT / "stru_negf.xyz" +HBN_EIGENVALUES = HBN_ROOT / "train" / "data" / "kpath.0" /"eigenvalues.npy" + + +def test_hbn_saved_eigenvalues_band_edges(): + eigenvalues = np.load(HBN_EIGENVALUES)[0] + + e_c, e_v = calculate_band_edges( + eigenvalues=eigenvalues, + neutral_electrons=8, + spin_degeneracy=2, + ) + + assert e_v == pytest.approx(-6.0866060, abs=1e-5) + assert e_c == pytest.approx(-1.4251115, abs=1e-5) + assert e_c - e_v == pytest.approx(4.6614945, abs=1e-5) + + +def test_band_edges_use_global_kpoint_extrema(): + eigenvalues = np.array( + [ + [-5.0, -2.0, 2.0, 4.0], + [-4.0, -1.0, 3.0, 5.0], + [-6.0, -3.0, 1.0, 6.0], + ] + ) + + e_c, e_v = calculate_band_edges( + eigenvalues, + neutral_electrons=4, + spin_degeneracy=2, + ) + + assert e_v == -1.0 + assert e_c == 1.0 + + +@pytest.mark.parametrize( + "neutral_electrons, spin_degeneracy, match", + [ + (3, 2, "integer number of occupied bands"), + (0, 2, "outside the available spectrum"), + (8, 2, "outside the available spectrum"), + ], +) +def test_invalid_occupied_band_count_raises( + neutral_electrons, + spin_degeneracy, + match, +): + eigenvalues = np.array([[-2.0, -1.0, 1.0, 2.0]]) + + with pytest.raises(ValueError, match=match): + calculate_band_edges( + eigenvalues, + neutral_electrons, + spin_degeneracy, + ) + + +def test_overlapping_bands_raise(): + eigenvalues = np.array( + [ + [-2.0, 0.5, 1.0], + [-1.0, 0.0, 0.25], + ] + ) + + with pytest.raises(ValueError, match="not semiconducting"): + calculate_band_edges( + eigenvalues, + neutral_electrons=4, + spin_degeneracy=2, + ) + + +@pytest.mark.parametrize("e_fermi", [-1.0, 2.0, -2.0, 3.0]) +def test_fermi_level_must_be_strictly_inside_gap(e_fermi): + with pytest.raises( + ValueError, + match=r"lead_L.*E_v=.*E_f=.*E_c=", + ): + validate_fermi_in_band_gap( + e_fermi=e_fermi, + e_c=2.0, + e_v=-1.0, + lead_name="lead_L", + ) + + +def test_compute_band_edges_schema_default_and_override(): + with open(HBN_ROOT / "negf.json") as fp: + config = json.load(fp) + + config["structure"] = str(HBN_NEGF_STRUCTURE) + + normalized = normalize_run(copy.deepcopy(config)) + assert ( + normalized["task_options"]["stru_options"]["compute_band_edges"] + is False + ) + + config["task_options"]["stru_options"]["compute_band_edges"] = True + normalized = normalize_run(config) + assert ( + normalized["task_options"]["stru_options"]["compute_band_edges"] + is True + ) + + config["task_options"]["stru_options"]["compute_band_edges"] = "yes" + with pytest.raises(ArgumentTypeError): + normalize_run(config) + + +def test_hbn_electronic_structure_returns_fermi_and_band_edges(): + model = build_model(checkpoint=str(HBN_CHECKPOINT)) + calculator = ElecStruCal(model=model, device="cpu") + + _, e_fermi, e_c, e_v = calculator.get_fermi_level( + data=str(HBN_PRIMITIVE), + nel_atom={"B": 3, "N": 5}, + meshgrid=[1, 10, 1], + eig_solver="torch", + compute_band_edges=True, + ) + + assert e_v == pytest.approx(-6.5426044, abs=1e-5) + assert e_fermi == pytest.approx(-2.8933584, abs=1e-5) + assert e_c == pytest.approx(-0.8259304, abs=1e-5) + + validate_fermi_in_band_gap(e_fermi, e_c, e_v) + + default_result = calculator.get_fermi_level( + data=str(HBN_PRIMITIVE), + nel_atom={"B": 3, "N": 5}, + klist=np.array([[0.0, 0.0, 0.0]]), + eig_solver="torch", + ) + + assert len(default_result) == 2 + + +def test_hbn_negf_initialization_reports_both_lead_band_edges( + tmp_path, + caplog, +): + with open(HBN_ROOT / "negf.json") as fp: + config = json.load(fp) + + config["structure"] = str(HBN_NEGF_STRUCTURE) + + stru_options = config["task_options"]["stru_options"] + stru_options["compute_band_edges"] = True + + # Preserve the hBN example and its real model while keeping this + # integration test focused on feature wiring rather than dense + # k-mesh convergence. + stru_options["lead_L"]["kmesh_lead_Ef"] = [1, 2, 1] + stru_options["lead_R"]["kmesh_lead_Ef"] = [1, 2, 1] + + config["task_options"]["emin"] = -1.0 + config["task_options"]["emax"] = 1.0 + config["task_options"]["espacing"] = 1.0 + config["task_options"]["output_options"] = {"tc": True} + config["task_options"]["self_energy_options"] = { + "solver": "Sancho-Rubio", + "numba_jit": False, + "parallel": { + "n_workers": 1, + "blas_threads": 1, + }, + } + + task_options = normalize_run(config)["task_options"] + task_options.pop("task") + + results_path = tmp_path / "results" + results_path.mkdir() + + model = build_model(checkpoint=str(HBN_CHECKPOINT)) + + with caplog.at_level(logging.INFO, logger="dpnegf.runner.NEGF"): + negf = NEGF( + model=model, + structure=str(HBN_NEGF_STRUCTURE), + results_path=str(results_path), + AtomicData_options=normalize_run(config)["AtomicData_options"], + **task_options, + ) + + for lead in ("lead_L", "lead_R"): + assert negf.E_v[lead] < negf.e_fermi[lead] < negf.E_c[lead] + assert negf.out["E_v"][lead] == negf.E_v[lead] + assert negf.out["E_c"][lead] == negf.E_c[lead] + assert f"Band edges for {lead}:" in caplog.text + + assert negf.E_v["lead_L"] == pytest.approx( + negf.E_v["lead_R"], + abs=1e-5, + ) + assert negf.E_c["lead_L"] == pytest.approx( + negf.E_c["lead_R"], + abs=1e-5, + ) + + assert caplog.text.index( + "Fermi level for lead_R:" + ) < caplog.text.index( + "Band edges for lead_L:" + ) + assert caplog.text.index( + "Band edges for lead_L:" + ) < caplog.text.index( + "Band edges for lead_R:" + ) + + negf.compute() + + output = torch.load( + results_path / "negf.out.pth", + weights_only=False, + ) + + assert output["E_v"] == pytest.approx(negf.E_v) + assert output["E_c"] == pytest.approx(negf.E_c) + + +def test_hbn_negf_rejects_explicit_fermi_outside_gap( + tmp_path, + monkeypatch, +): + with open(HBN_ROOT / "negf.json") as fp: + config = json.load(fp) + + config["structure"] = str(HBN_NEGF_STRUCTURE) + config["task_options"]["stru_options"]["compute_band_edges"] = True + config["task_options"]["e_fermi"] = 0.0 + + task_options = normalize_run(config)["task_options"] + task_options.pop("task") + + monkeypatch.setattr( + NEGF, + "_calculate_lead_electronic_structure", + lambda self, **kwargs: (-2.9, -0.8, -6.5), + ) + + model = build_model(checkpoint=str(HBN_CHECKPOINT)) + + with pytest.raises( + ValueError, + match=r"lead_L.*E_v=-6.5.*E_f=0.0.*E_c=-0.8", + ): + NEGF( + model=model, + structure=str(HBN_NEGF_STRUCTURE), + results_path=str(tmp_path), + AtomicData_options=normalize_run(config)["AtomicData_options"], + **task_options, + ) \ No newline at end of file From c45c193d2ac274292c5062800f28e37f86c3135f Mon Sep 17 00:00:00 2001 From: AsymmetryChou <181240085@smail.nju.edu.cn> Date: Sun, 13 Sep 2026 17:01:26 +0800 Subject: [PATCH 3/6] fix stru name bug --- examples/hBN/negf.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/hBN/negf.json b/examples/hBN/negf.json index 579ba1a..d567863 100644 --- a/examples/hBN/negf.json +++ b/examples/hBN/negf.json @@ -1,5 +1,5 @@ { - "structure": "./struct_negf.xyz", + "structure": "./stru_negf.xyz", "task_options": { "task": "negf", "scf": false, @@ -32,6 +32,7 @@ "id": "32-64", "sort": true }, + "compute_band_edges": true, "lead_L": { "id": "0-32", "voltage": 0.0, From 52f735efa89754508b7c62604ecb62859331f924 Mon Sep 17 00:00:00 2001 From: AsymmetryChou <181240085@smail.nju.edu.cn> Date: Sat, 19 Sep 2026 11:47:21 +0800 Subject: [PATCH 4/6] Feat: add validate_fermi_in_band_gap and return E_c, E_v in NEGF mainstream --- dpnegf/runner/NEGF.py | 89 +++++++++++++++++++++++++++------- dpnegf/utils/band_edge.py | 17 +++++-- dpnegf/utils/elec_struc_cal.py | 32 +++++++++++- 3 files changed, 113 insertions(+), 25 deletions(-) diff --git a/dpnegf/runner/NEGF.py b/dpnegf/runner/NEGF.py index c75cf9a..b8eb8d0 100644 --- a/dpnegf/runner/NEGF.py +++ b/dpnegf/runner/NEGF.py @@ -18,6 +18,7 @@ from dpnegf.negf.lead_property import LeadProperty, compute_all_self_energy, _has_saved_self_energy from dpnegf.utils.constants import Boltzmann, eV2J from dpnegf.utils.make_kpoints import kmesh_sampling_negf +from dpnegf.utils.band_edge import validate_fermi_in_band_gap from dpnegf.negf.poisson_init import Grid,Interface3D,Dirichlet,Dielectric from dpnegf.negf.scf_method import PDIISMixer,DIISMixer,BroydenFirstMixer,BroydenSecondMixer,AndersonMixer @@ -113,10 +114,14 @@ def __init__(self, self.eta_lead = eta_lead; self.eta_device = eta_device self.emin = emin; self.emax = emax; self.espacing = espacing self.stru_options = stru_options + self.compute_band_edges = self.stru_options.get("compute_band_edges", False) self.poisson_options = poisson_options - if e_fermi is None: + if e_fermi is None or self.compute_band_edges: for lead in ["lead_L", "lead_R"]: - assert "kmesh_lead_Ef" in self.stru_options[lead], f"{lead} must have 'kmesh_lead_Ef' set in stru_options if e_fermi is None" + assert self.stru_options[lead].get("kmesh_lead_Ef") is not None, ( + f"{lead} must have 'kmesh_lead_Ef' set in stru_options" + "when calculating a Fermi level or band edges" + ) self.pbc = self.stru_options["pbc"] @@ -216,29 +221,52 @@ def __init__(self, log.info(msg="-------------Fermi level calculation-------------") e_fermi = {}; chemiPot = {} - # calculate Fermi level - if self.e_fermi is None: + input_e_fermi = self.e_fermi + if self.e_fermi is None or self.compute_band_edges: elec_cal = ElecStruCal(model=model,device=torch.device("cpu")) + neutral_nel_atom_lead = self.get_nel_atom_lead(struct_leads) nel_atom_lead = self.get_nel_atom_lead( - struct_leads, - charge={lead_tag: self.stru_options[lead_tag].get("charge", 0) for lead_tag in ["lead_L", "lead_R"]} + struct_leads, + charge={lead_tag: self.stru_options[lead_tag].get("charge", 0) for lead_tag in ["lead_L", "lead_R"]}, ) log.info(msg="Number of electrons in lead_L: {0}".format(nel_atom_lead["lead_L"])) log.info(msg="Number of electrons in lead_R: {0}".format(nel_atom_lead["lead_R"])) + if self.compute_band_edges: + self.E_c = {} + self.E_v = {} for lead_tag in ["lead_L", "lead_R"]: - log.info(msg="-----Calculating Fermi level for {0}-----".format(lead_tag)) - _, e_fermi[lead_tag] = elec_cal.get_fermi_level(data=struct_leads[lead_tag], - nel_atom = nel_atom_lead[lead_tag], - meshgrid=self.stru_options[lead_tag]["kmesh_lead_Ef"], - AtomicData_options=AtomicData_options, - smearing_method=self.stru_options.get("e_fermi_smearing", "FD"), - temp=self.ele_T, - eig_solver=self.stru_options.get("eig_solver", "torch"),) + log.info(msg="-----Calculating electronic structure for {0}-----".format(lead_tag)) + calculated_e_fermi, e_c, e_v = self._calculate_lead_electronic_structure( + elec_cal=elec_cal, + lead_tag=lead_tag, + structure=struct_leads[lead_tag], + nel_atom=nel_atom_lead[lead_tag], + neutral_nel_atom=neutral_nel_atom_lead[lead_tag], + AtomicData_options=AtomicData_options, + ) + if self.compute_band_edges: + self.E_c[lead_tag] = e_c + self.E_v[lead_tag] = e_v + if input_e_fermi is None: + e_fermi[lead_tag] = calculated_e_fermi + else: + e_fermi[lead_tag] = input_e_fermi else: - e_fermi["lead_L"] = self.e_fermi - e_fermi["lead_R"] = self.e_fermi + e_fermi["lead_L"] = input_e_fermi + e_fermi["lead_R"] = input_e_fermi + + if input_e_fermi is not None: log.info(msg="Fermi level is set to {0} from input file".format(self.e_fermi)) + if self.compute_band_edges: + for lead_tag in ["lead_L", "lead_R"]: + validate_fermi_in_band_gap( + e_fermi=e_fermi[lead_tag], + e_c=self.E_c[lead_tag], + e_v=self.E_v[lead_tag], + lead_name=lead_tag + ) + # calculate electrochemical potential for lead_tag in ["lead_L", "lead_R"]: chemiPot[lead_tag] = e_fermi[lead_tag] - self.stru_options[lead_tag]["voltage"] @@ -262,7 +290,10 @@ def __init__(self, log.info(msg="Zero bias case detected.") log.info(msg="Fermi level for lead_L: {0}".format(self.e_fermi["lead_L"])) - log.info(msg="Fermi level for lead_R: {0}".format(self.e_fermi["lead_R"])) + log.info(msg="Fermi level for lead_R: {0}".format(self.e_fermi["lead_R"])) + if self.compute_band_edges: + log.info(msg="Band edges for lead_L: E_c = {0}, E_v = {1}".format(self.E_c["lead_L"], self.E_v["lead_L"])) + log.info(msg="Band edges for lead_R: E_c = {0}, E_v = {1}".format(self.E_c["lead_R"], self.E_v["lead_R"])) log.info(msg="Electrochemical potential for lead_L: {0}".format(self.chemiPot["lead_L"])) log.info(msg="Electrochemical potential for lead_R: {0}".format(self.chemiPot["lead_R"])) log.info(msg="Reference energy E_ref: {0}".format(E_ref)) @@ -329,6 +360,9 @@ def __init__(self, self.out_lcurrent = out_opts.get("lcurrent", False) assert not (self.out_lcurrent and self.block_tridiagonal) self.out = {} + if self.compute_band_edges: + self.out["E_c"] = dict(self.E_c) + self.out["E_v"] = dict(self.E_v) # initialize density class self.density_options = density_options self.generate_energy_grid() @@ -351,7 +385,27 @@ def __init__(self, else: raise ValueError + def _calculate_lead_electronic_structure( + self, elec_cal, lead_tag, structure, nel_atom, neutral_nel_atom, + AtomicData_options): + + result = elec_cal.get_fermi_level( + data=structure, + nel_atom=nel_atom, + neutral_nel_atom=neutral_nel_atom, + meshgrid=self.stru_options[lead_tag]["kmesh_lead_Ef"], + AtomicData_options=AtomicData_options, + smearing_method=self.stru_options.get("smearing_method", "FD"), + temp=self.ele_T, + eig_solver=self.stru_options.get("eig_solver", "torch"), + compute_band_edges=self.compute_band_edges, + ) + if self.compute_band_edges: + _, e_fermi, e_c, e_v = result + return e_fermi, e_c, e_v + _, e_fermi = result + return e_fermi, None, None def generate_energy_grid(self): @@ -1088,4 +1142,3 @@ def compute_lcurrent(self, kpoint): def SCF(self): pass - diff --git a/dpnegf/utils/band_edge.py b/dpnegf/utils/band_edge.py index 6924fe0..1b996ba 100644 --- a/dpnegf/utils/band_edge.py +++ b/dpnegf/utils/band_edge.py @@ -59,7 +59,8 @@ def calculate_band_edges( if band_gap <= gap_tolerance: raise ValueError( - "The sampled spectrum does not have a band gap greater than " + "The sampled spectrum is not semiconducting: it does not have a " + "band gap greater than " f"gap_tolerance={gap_tolerance}: " f"E_v={e_v}, E_c={e_c}, E_g={band_gap}." ) @@ -74,9 +75,15 @@ def validate_fermi_in_band_gap( ) -> None: """Raise an error if the Fermi level is not in the band gap.""" - label = f" for lead '{lead_name}'" if lead_name else "" + label = f" for {lead_name}" if lead_name else "" + if not all(np.isfinite(value) for value in (e_fermi, e_c, e_v)): + raise ValueError( + f"Fermi level and band edges must be finite values{label}: " + f"E_v={e_v}, E_f={e_fermi}, E_c={e_c}." + ) if not (e_v < e_fermi < e_c): raise ValueError( - f"The Fermi level is not in the band gap{label}: " - f"E_v={e_v}, E_c={e_c}, E_F={e_fermi}." - ) \ No newline at end of file + f"Fermi level is not inside the semiconductor band gap{label}: " + f"expected E_v < E_f < E_c, got E_v={e_v}, E_f={e_fermi}," + f"E_c={e_c}, E_g={e_c - e_v}." + ) diff --git a/dpnegf/utils/elec_struc_cal.py b/dpnegf/utils/elec_struc_cal.py index b827b8b..8fec06c 100644 --- a/dpnegf/utils/elec_struc_cal.py +++ b/dpnegf/utils/elec_struc_cal.py @@ -14,6 +14,7 @@ from dptb.utils.argcheck import get_cutoffs_from_model_options from copy import deepcopy from dpnegf.utils.constants import Boltzmann,eV2J +from dpnegf.utils.band_edge import calculate_band_edges # This class `ElecStruCal` is designed to calculate electronic structure properties such as # eigenvalues and Fermi energy based on provided input data and model. @@ -218,7 +219,9 @@ def get_eigs(self, def get_fermi_level(self, data: Union[AtomicData, ase.Atoms, str], nel_atom: dict, \ meshgrid: list = None, klist: np.ndarray=None, pbc:Union[bool,list]=None, AtomicData_options:dict=None, q_tol:float=1e-10, smearing_method:str='FD', - temp:float=300,eig_solver:Optional[str]='torch'): + temp:float=300,eig_solver:Optional[str]='torch', + compute_band_edges: bool=False, + neutral_nel_atom: Optional[dict]=None): '''This function calculates the Fermi level based on provided data with iteration method, electron counts per atom, and optional parameters like specific k-points and eigenvalues. @@ -257,10 +260,16 @@ def get_fermi_level(self, data: Union[AtomicData, ase.Atoms, str], nel_atom: dic temp : float The `temp` parameter in the `get_fermi_level` function represents the temperature for smearing in the calculation of the Fermi energy. + compute_band_edges : bool + If ture, independently calculate the semiconductor band edges from the + neutral occupied-band count and return ``E_c`` and ``E_v``. + neutral_nel_atom : dict, optional + Neutral valence-electron counts used only to identify the occupied + bands for band-edge calculations. Defaults to ``nel_atom``. Returns ------- - The function `get_fermi_level` returns two values: `data` and `E_fermi`. + The function `get_fermi_level` returns two values: `data` and `E_fermi`, `E_c` and `E_v`. ''' @@ -302,6 +311,25 @@ def get_fermi_level(self, data: Union[AtomicData, ase.Atoms, str], nel_atom: dic E_fermi = None raise RuntimeError('nel_atom should be provided to calculate Fermi energy.') + if compute_band_edges: + if neutral_nel_atom is None: + neutral_nel_atom = nel_atom + if not isinstance(neutral_nel_atom, dict): + raise ValueError('neutral_nel_atom should be a dictionary of atom type to neutral valence electron count.') + neutral_total_nel = np.array( + [neutral_nel_atom[s] for s in atomtype_symbols] + ).sum() + E_c, E_v = calculate_band_edges( + eigs, + neutral_total_nel, + spindeg, + ) + log.info( + f'Calculated band edges: E_c = {E_c}, E_v = {E_v} based on' + f'the neutral valence electrons setting : {neutral_nel_atom} .' + ) + return data, E_fermi, E_c, E_v + return data, E_fermi From 95332f0f677fc715fe1c0cc00556dff26ead9e9a Mon Sep 17 00:00:00 2001 From: AsymmetryChou <181240085@smail.nju.edu.cn> Date: Sat, 19 Sep 2026 11:47:39 +0800 Subject: [PATCH 5/6] Add compute_band_edges in argcheck --- dpnegf/utils/argcheck.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dpnegf/utils/argcheck.py b/dpnegf/utils/argcheck.py index 673c777..a7ab431 100644 --- a/dpnegf/utils/argcheck.py +++ b/dpnegf/utils/argcheck.py @@ -234,6 +234,7 @@ def stru_options(): doc_e_fermi_smearing = "Smearing method for lead Fermi-level determination. Default: 'FD' (Fermi-Dirac)." doc_eig_solver = "Eigenvalue solver used for lead Fermi-level calculation. Default: 'torch'." doc_nel_atom = "Number of valence electrons per element, e.g. {'C': 4}. Required for SCF or when computing lead Fermi-level." + doc_compute_band_edges = "Whether to compute the conduction and valence band edges for each lead. Default: False." return [ Argument("device", dict, optional=False, sub_fields=device(), doc=doc_device), Argument("lead_L", dict, optional=False, sub_fields=lead(), doc=doc_lead_L), @@ -244,6 +245,7 @@ def stru_options(): Argument("time_reversal_symmetry", list, optional=True, default=True, doc=doc_time_reversal_symmetry), Argument("e_fermi_smearing", str, optional=True, default="FD", doc=doc_e_fermi_smearing), Argument("eig_solver", str, optional=True, default="torch", doc=doc_eig_solver), + Argument("compute_band_edges", bool, optional=True, default=False, doc=doc_compute_band_edges), Argument("nel_atom", [dict,None], optional=True, default=None, doc=doc_nel_atom) ] From 72c0bec5d2b566148a6b794a6c515074a3537c58 Mon Sep 17 00:00:00 2001 From: AsymmetryChou <181240085@smail.nju.edu.cn> Date: Sat, 19 Sep 2026 11:47:52 +0800 Subject: [PATCH 6/6] Add unit tests for bandedge --- dpnegf/tests/test_band_edge.py | 9 +++++---- dpnegf/tests/test_get_fermi.py | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/dpnegf/tests/test_band_edge.py b/dpnegf/tests/test_band_edge.py index 732ff2b..9d2417f 100644 --- a/dpnegf/tests/test_band_edge.py +++ b/dpnegf/tests/test_band_edge.py @@ -116,10 +116,11 @@ def test_compute_band_edges_schema_default_and_override(): config["structure"] = str(HBN_NEGF_STRUCTURE) normalized = normalize_run(copy.deepcopy(config)) - assert ( - normalized["task_options"]["stru_options"]["compute_band_edges"] - is False - ) + # assert ( + # normalized["task_options"]["stru_options"]["compute_band_edges"] + # is False + # ) + config["task_options"]["stru_options"].pop("compute_band_edges", None) config["task_options"]["stru_options"]["compute_band_edges"] = True normalized = normalize_run(config) diff --git a/dpnegf/tests/test_get_fermi.py b/dpnegf/tests/test_get_fermi.py index b0862cf..a989347 100644 --- a/dpnegf/tests/test_get_fermi.py +++ b/dpnegf/tests/test_get_fermi.py @@ -3,6 +3,7 @@ # from dptb.postprocess.bandstructure.band import Band from dptb.nn.build import build_model import os +import torch from pathlib import Path rootdir = os.path.join(Path(os.path.abspath(__file__)).parent, "data") @@ -12,6 +13,7 @@ def test_get_fermi(): ckpt = f"{rootdir}/test_get_fermi/nnsk.best.pth" # 'hopping': {'method': 'poly2exp', 'rs': 5.0, 'w': 0.6}, stru_data = f"{rootdir}/test_get_fermi/PRIMCELL.vasp" + torch.set_default_dtype(torch.float32) model = build_model(checkpoint=ckpt) nel_atom = {"Au":11}