Skip to content

Add eigenvalue solver option to structure calculations#46

Merged
AsymmetryChou merged 2 commits into
DeePTB-Lab:mainfrom
AsymmetryChou:eig_np
Nov 28, 2025
Merged

Add eigenvalue solver option to structure calculations#46
AsymmetryChou merged 2 commits into
DeePTB-Lab:mainfrom
AsymmetryChou:eig_np

Conversation

@AsymmetryChou

@AsymmetryChou AsymmetryChou commented Nov 28, 2025

Copy link
Copy Markdown
Contributor

Introduce an eigenvalue solver option in the structure calculation module, allowing users to choose between 'torch' and 'numpy' solvers. Update example JSON and related tests to reflect this new functionality. Note that this function should be compatible with the corresponding version of DeePTB with numpy solver.

Summary by CodeRabbit

  • New Features

    • Configurable eigen-solver selection (torch or numpy) for Fermi level calculations
    • Support for overriding overlap matrices from external files during computations
    • New eig_solver configuration option available in structure settings
  • Tests

    • Expanded test coverage for multiple eigen-solver configurations and smearing methods

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Nov 28, 2025

Copy link
Copy Markdown

Walkthrough

This pull request introduces configurable eigenvalue solver selection and external overlap matrix override capabilities throughout the NEGF calculation pipeline. The eig_solver parameter is threaded through the runner, core computation methods, and test suite, with additional support for override_overlap handling in eigenvalue and Fermi level calculations.

Changes

Cohort / File(s) Summary
Core API Extensions
dpnegf/utils/elec_struc_cal.py
Extended get_data(), get_eigs(), and get_fermi_level() signatures to accept eig_solver and override_overlap parameters; added logic to load external overlap matrices, apply them to model computations, and restore original overlap metadata post-computation.
Configuration & Argument Schema
dpnegf/utils/argcheck.py, examples/atomic_chain_api/input_files/negf_chain_new.json
Added eig_solver configuration field to stru_options schema with default value "torch" and corresponding example config file entry.
NEGF Runner Integration
dpnegf/runner/NEGF.py
Updated Fermi level calculation call to pass eig_solver from stru_options to elec_cal.get_fermi_level().
Test & Import Updates
dpnegf/tests/test_get_fermi.py
Changed import source for ElecStruCal from dpnegf.utils to dptb.postprocess; removed root_directory fixture parameter; expanded test coverage to exercise multiple smearing methods and explicit eig_solver configurations (torch, numpy) with updated expected Fermi level values.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • dpnegf/utils/elec_struc_cal.py requires careful attention: verify that override_overlap file I/O is robust, that edge/node overlap restoration logic properly preserves state, and that parameter propagation to nested get_eigs() calls is correct.
  • dpnegf/tests/test_get_fermi.py import path change should be verified to confirm the new import source exists and is the intended location.
  • Confirm that the eig_solver default value "torch" is applied consistently across all call sites in NEGF.py and other entry points.

Poem

🐰 A solver that bends to our will,
Through torch or numpy with skill,
Overlaps now flow where we choose,
No quantum secrets to lose,
Our Fermi dance grows more spry!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding an eigenvalue solver option (torch/numpy choice) to structure calculations across NEGF module.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
dpnegf/utils/argcheck.py (1)

1079-1090: Consider adding validation for allowed eig_solver values.

The argument accepts any string, but only "torch" and "numpy" are valid values. Consider using an explicit list of allowed values to fail-fast on invalid input.

     doc_eig_solver="The eigenvalue solver to use."
+    doc_eig_solver="The eigenvalue solver to use. Options: 'torch' or 'numpy'."
     doc_nel_atom = "The number of electrons in each element."
     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),
         Argument("lead_R", dict, optional=False, sub_fields=lead(), doc=doc_lead_R),
         Argument("kmesh", list, optional=True, default=[1,1,1], doc=doc_kmesh),
         Argument("pbc", list, optional=True, default=[False, False, False], doc=doc_pbc),
         Argument("gamma_center", list, optional=True, default=True, doc=doc_gamma_center),
         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("eig_solver", str, optional=True, default="torch", alias=["torch", "numpy"], doc=doc_eig_solver),
         Argument("nel_atom", [dict,None], optional=True, default=None, doc=doc_nel_atom)
     ]

Note: Verify if dargs.Argument supports an alias or similar parameter for value validation. If not, validation could be added in the consuming code.

dpnegf/tests/test_get_fermi.py (1)

11-37: Tests provide good coverage for both solvers.

The test properly exercises both eig_solver options ("torch" and "numpy") with both smearing methods. All four test cases correctly expect the same Fermi level value, validating solver equivalence.

Consider using pytest.mark.parametrize to reduce code duplication and make test failures more specific.

import pytest

@pytest.mark.parametrize("smearing_method,eig_solver", [
    ("FD", "torch"),
    ("Gaussian", "torch"),
    ("FD", "numpy"),
    ("Gaussian", "numpy"),
])
def test_get_fermi(smearing_method, eig_solver):
    ckpt = f"{rootdir}/test_get_fermi/nnsk.best.pth"
    stru_data = f"{rootdir}/test_get_fermi/PRIMCELL.vasp"

    model = build_model(checkpoint=ckpt)
    nel_atom = {"Au":11}

    elec_cal = ElecStruCal(model=model, device='cpu')
    _, efermi = elec_cal.get_fermi_level(
        data=stru_data, 
        nel_atom=nel_atom,
        smearing_method=smearing_method,
        meshgrid=[30,30,30],
        eig_solver=eig_solver
    )
    assert abs(efermi + 3.2262574434280395) < 1e-3
dpnegf/utils/elec_struc_cal.py (3)

71-76: Update type hints to use explicit Optional syntax.

Several parameters use implicit Optional (e.g., pbc: Union[bool, list] = None). Per PEP 484, use explicit Optional[T] or T | None for nullable parameters.

     def get_data(self,
                  data: Union[AtomicData, ase.Atoms, str],
-                 pbc:Union[bool,list]=None,
-                 device: Union[str, torch.device]=None,
-                 AtomicData_options:dict=None,
+                 pbc: Optional[Union[bool, list]] = None,
+                 device: Optional[Union[str, torch.device]] = None,
+                 AtomicData_options: Optional[dict] = None,
                  override_overlap:Optional[str]=None):

204-214: Backup/restore pattern for overlap data is fragile.

The code saves EDGE_OVERLAP_KEY and NODE_OVERLAP_KEY before self.model(data) and restores them after, suggesting the model call overwrites these fields. This workaround is necessary but could break if model behavior changes.

Consider adding a comment explaining why this is needed.

         if isinstance(override_overlap, str):
+            # Store override overlap data before model call, as model may overwrite these fields
             override_overlap_edge = data[AtomicDataDict.EDGE_OVERLAP_KEY]
             override_overlap_node = data[AtomicDataDict.NODE_OVERLAP_KEY]
         # get the eigenvalues
         data = self.model(data)
         if isinstance(override_overlap, str):
+            # Restore override overlap data after model call
             data[AtomicDataDict.EDGE_OVERLAP_KEY] = override_overlap_edge
             data[AtomicDataDict.NODE_OVERLAP_KEY] = override_overlap_node

218-221: Consider adding validation for eig_solver parameter.

The eig_solver parameter is passed directly to self.eigv() without validation. If an invalid value is passed, the error will occur deep in the eigenvalue calculation rather than at the interface.

     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'):
+                        meshgrid: Optional[list] = None, klist: Optional[np.ndarray] = None, 
+                        pbc: Optional[Union[bool, list]] = None,
+                        AtomicData_options: Optional[dict] = None, q_tol: float = 1e-10, 
+                        smearing_method: str = 'FD', temp: float = 300,
+                        eig_solver: str = 'torch'):
         '''...'''
+        if eig_solver not in ('torch', 'numpy'):
+            raise ValueError(f"eig_solver must be 'torch' or 'numpy', got '{eig_solver}'")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c0c161 and f4343b6.

📒 Files selected for processing (5)
  • dpnegf/runner/NEGF.py (1 hunks)
  • dpnegf/tests/test_get_fermi.py (1 hunks)
  • dpnegf/utils/argcheck.py (2 hunks)
  • dpnegf/utils/elec_struc_cal.py (7 hunks)
  • examples/atomic_chain_api/input_files/negf_chain_new.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
dpnegf/utils/elec_struc_cal.py (1)
dpnegf/data/interfaces/ham_to_feature.py (1)
  • block_to_feature (11-173)
🪛 Ruff (0.14.6)
dpnegf/utils/elec_struc_cal.py

6-6: Redefinition of unused np from line 3

Remove definition: np

(F811)


73-73: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


75-75: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


176-176: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


177-177: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


219-219: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


219-219: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


220-220: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: run-tests
🔇 Additional comments (3)
examples/atomic_chain_api/input_files/negf_chain_new.json (1)

22-22: LGTM!

The new eig_solver configuration option is correctly added to the example file, demonstrating how users can select the numpy solver.

dpnegf/runner/NEGF.py (1)

180-186: LGTM!

The eig_solver parameter is correctly retrieved from stru_options with a sensible default of "torch" for backward compatibility, and properly passed to get_fermi_level.

dpnegf/tests/test_get_fermi.py (1)

1-1: I'm unable to access the repository (DeePTB-Lab/dpnegf) to verify the import path change. Both direct cloning and GitHub CLI methods failed, likely due to repository access restrictions or availability issues.

Since I cannot verify the concern about the import path change from dpnegf.utils.elec_struc_cal to dptb.postprocess.elec_struc_cal through automated means, here is the rewritten review comment:


Verify import path change is intentional and correct.

The import changed from dpnegf.utils.elec_struc_cal to dptb.postprocess.elec_struc_cal. Please confirm this reflects an intended refactoring (e.g., moving the module between packages) and that the ElecStruCal class is now located in the dptb.postprocess module rather than dpnegf.utils.


Comment on lines 3 to 6
import numpy as np
from ase.io import read
import ase
import numpy as np

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove duplicate numpy import.

numpy is imported twice: on line 3 and line 6. The second import is redundant.

 import numpy as np
 from ase.io import read
 import ase
-import numpy as np
 from typing import Union
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import numpy as np
from ase.io import read
import ase
import numpy as np
import numpy as np
from ase.io import read
import ase
🧰 Tools
🪛 Ruff (0.14.6)

6-6: Redefinition of unused np from line 3

Remove definition: np

(F811)

🤖 Prompt for AI Agents
In dpnegf/utils/elec_struc_cal.py around lines 3 to 6, numpy is imported twice
(lines 3 and 6); remove the redundant second "import numpy as np" so numpy is
only imported once, keeping the existing first import and leaving the other
imports unchanged.

Comment on lines +142 to +163
if isinstance(override_overlap, str):
assert os.path.exists(override_overlap), "Overlap file not found."
overlap_blocks = h5py.File(override_overlap, "r")
if len(overlap_blocks) != 1:
log.info('Overlap file contains more than one overlap matrix, only first will be used.')
if self.overlap:
log.warning('override_overlap is enabled while model contains overlap, override_overlap will be used.')
if "0" in overlap_blocks:
overlaps = overlap_blocks["0"]
else:
overlaps = overlap_blocks["1"]
block_to_feature(data, self.model.idp, blocks=False, overlap_blocks=overlaps)
if not self.overlap:
self.eigv = Eigenvalues(
idp=self.model.idp,
device=self.device,
s_edge_field=AtomicDataDict.EDGE_OVERLAP_KEY,
s_node_field=AtomicDataDict.NODE_OVERLAP_KEY,
s_out_field=AtomicDataDict.OVERLAP_KEY,
dtype=self.model.dtype,
)
overlap_blocks.close()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential state mutation issue with self.eigv modification.

When override_overlap is provided and self.overlap is False, a new Eigenvalues instance is assigned to self.eigv. This modifies the instance state, which could cause unexpected behavior in subsequent calls if the caller expects the original non-overlap eigv.

Consider using a local variable instead of modifying instance state, or document this behavior clearly.

+        eigv_to_use = self.eigv  # default to instance eigv
         if isinstance(override_overlap, str):
             assert os.path.exists(override_overlap), "Overlap file not found."
             overlap_blocks = h5py.File(override_overlap, "r")
             if len(overlap_blocks) != 1:
                 log.info('Overlap file contains more than one overlap matrix, only first will be used.')
             if self.overlap:
                 log.warning('override_overlap is enabled while model contains overlap, override_overlap will be used.')
             if "0" in overlap_blocks:
                 overlaps = overlap_blocks["0"]
             else:
                 overlaps = overlap_blocks["1"]
             block_to_feature(data, self.model.idp, blocks=False, overlap_blocks=overlaps)
             if not self.overlap:
-                self.eigv = Eigenvalues(
+                eigv_to_use = Eigenvalues(
                     idp=self.model.idp,
                     device=self.device,
                     s_edge_field=AtomicDataDict.EDGE_OVERLAP_KEY,
                     s_node_field=AtomicDataDict.NODE_OVERLAP_KEY,
                     s_out_field=AtomicDataDict.OVERLAP_KEY,
                     dtype=self.model.dtype,
                 )
             overlap_blocks.close()

Note: This would require returning the eigv_to_use from get_data or restructuring the code flow.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In dpnegf/utils/elec_struc_cal.py around lines 142 to 163, the code assigns a
new Eigenvalues instance to self.eigv when override_overlap is provided and
self.overlap is False, mutating object state; change this to create a local
variable (e.g., eigv_to_use) instead of reassigning self.eigv and wire that
local through the remainder of the function (or return it from get_data) so the
instance state is not mutated unexpectedly; ensure any downstream calls use the
local eigv_to_use and remove/avoid modifying self.eigv unless intentional and
documented.

@AsymmetryChou AsymmetryChou merged commit faafb4e into DeePTB-Lab:main Nov 28, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant