Skip to content

Commit 5f0f614

Browse files
authored
Added model export functionality to convert trained models to Wannier90 and PythTB formats. (#300)
* feat: Add export to Wannier90 and PythTB functionality * Add exmple for topythtb * feat: update PythTB export functionality * update docs * fix towannier * refactor: remove debug prints from load_data_for_model function Removed multiple debug print statements that were used for troubleshooting data object attributes and transformations. The debug output was checking for cell attribute existence, data object keys, and transformation results. These temporary debugging statements are no longer needed and have been cleaned up to improve code readability. * refactor: simplify ToWannier90 data handling and structure reconstruction Remove complex ASE atoms reconstruction logic in _get_data_and_blocks method. Store essential data (positions, cell, atom types, symbols) as instance attributes for reuse. Simplify return values by removing structase parameter. Streamline orbital mapping and hopping collection logic in write_hr method. * refactor: reorganize examples directory structure and notebooks - Rename ToPythTB directory to ToW90_PythTB for clarity - Move and update ToPythTB.ipynb with corrected paths and execution counts - Add new ToW90_pythtb.ipynb for Wannier90 export verification - Update .gitignore to exclude example output files (*.xyz, *.win) * docs: add Wannier90 API example notebook Add a new Jupyter notebook demonstrating how to export DeePTB models to Wannier90 format and validate the results by comparing band structures. The notebook includes step-by-step instructions for loading a model, exporting to _hr.dat format, and verifying correctness through PythTB interface comparison. Update the API documentation index to include the new example. * test: Install optional pythtb dependencies for testing. * fix: remove redundant multiplication by zero in Fermi energy subtraction The multiplication by 0 in the Fermi energy subtraction was redundant and has been removed. This simplifies the code without changing its behavior. * feat: refactor orbital handling and improve file resource management - Add get_orbitals_for_type helper function to centralize orbital expansion logic - Update load_data_for_model to use context manager for HDF5 file handling - Refactor ToWannier90 class to use new orbital helper function - Improve type hints with Optional for better parameter clarity - Fix trailing whitespace in documentation file * feat: simplify Wannier90 export interface Remove redundant struct_path parameter from write_win and write_centres methods in ToWannier90 class. Update export entrypoint and documentation notebooks to reflect the simplified API. Also improve device handling in ToPythTB class with better default value management. * refactor: simplify Wannier90 export method calls Remove redundant structure file parameter from write_win and write_centres methods as it's already handled internally by the exporter. This streamlines the API and reduces parameter duplication.
1 parent be2c8e8 commit 5f0f614

21 files changed

Lines changed: 2478 additions & 276 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
test*.ipynb
2+
examples/**/*centres.xyz
3+
examples/**/*.win
24
**/processed*/*
35
dptb/tests/**/*.yaml
46
dptb/tests/**/pre_*.pt

docs/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ For more details, see our papers:
4040
quick_start/easy_install
4141
quick_start/input
4242
quick_start/hands_on/index
43-
quick_start/basic_api
43+
quick_start/api/index
4444

4545

4646
.. toctree::
File renamed without changes.

docs/quick_start/api/index.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
=================================================
2+
A quick Example
3+
=================================================
4+
5+
.. toctree::
6+
basic_api
7+
pythtb_api
8+
wan90_api
9+

docs/quick_start/api/pythtb_api.ipynb

Lines changed: 460 additions & 0 deletions
Large diffs are not rendered by default.

docs/quick_start/api/wan90_api.ipynb

Lines changed: 387 additions & 0 deletions
Large diffs are not rendered by default.

dptb/entrypoints/export.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import json
2+
import torch
3+
import os
4+
import argparse
5+
import logging
6+
from dptb.nn.build import build_model
7+
from dptb.utils.tools import j_loader, j_must_have
8+
from dptb.postprocess.interfaces import ToWannier90, ToPythTB
9+
10+
log = logging.getLogger(__name__)
11+
12+
def export(INPUT: str, init_model: str = None, structure: str = None, output: str = "./", format: str = "wannier90", **kwargs):
13+
"""
14+
Export the DeePTB model to external formats.
15+
"""
16+
17+
# 1. Load config
18+
if INPUT and INPUT.endswith(".json"):
19+
jdata = j_loader(INPUT)
20+
else:
21+
# Fallback if no json provided (though usually required)
22+
# If user provides structure + model but no config?
23+
# DeePTB typically needs config for model architecture parameters stored in it (or in checkpoint?)
24+
# build_model usually needs common_options.
25+
# If init_model is a .pth, it might contain config?
26+
# dptb.nn.build.build_model(checkpoint=...) handles loading from checkpoint which might have config.
27+
jdata = {}
28+
29+
# Determine device
30+
device_str = jdata.get("device", "cpu")
31+
device = torch.device(device_str)
32+
33+
# Load model
34+
ckpt_path = init_model if init_model else jdata.get("model_ckpt")
35+
if not ckpt_path:
36+
log.error("Model checkpoint not found. Provide via -i or in config json via 'model_ckpt'.")
37+
return
38+
39+
# Common options for build_model
40+
# We might need to extract them from jdata or checkpoint
41+
# build_model(checkpoint=...) attempts to load config from checkpoint if available?
42+
# run.py passes in_common_options loaded from json.
43+
44+
in_common_options = {}
45+
if jdata.get("device"): in_common_options["device"] = jdata["device"]
46+
if jdata.get("dtype"): in_common_options["dtype"] = jdata["dtype"]
47+
48+
try:
49+
model = build_model(checkpoint=ckpt_path, common_options=in_common_options)
50+
model.to(device)
51+
model.eval()
52+
except Exception as e:
53+
log.error(f"Failed to load model: {e}")
54+
return
55+
56+
# Determine structure input
57+
struct_path = structure if structure else jdata.get("structure")
58+
if not struct_path:
59+
log.error("Structure file not specified. Provide via -stu or in config json.")
60+
return
61+
62+
# Ensure output directory exists
63+
if not os.path.exists(output):
64+
os.makedirs(output)
65+
66+
log.info(f"Loaded model from {ckpt_path}")
67+
log.info(f"Processing structure {struct_path}")
68+
log.info(f"Exporting to format: {format}")
69+
70+
# 2. Dispatch to Interface
71+
72+
if format.lower() in ["wannier90", "w90", "tb2j", "wannierberri"]:
73+
exporter = ToWannier90(model, device=device)
74+
75+
prefix = os.path.splitext(os.path.basename(struct_path))[0]
76+
hr_file = os.path.join(output, f"{prefix}_hr.dat")
77+
win_file = os.path.join(output, f"{prefix}.win")
78+
cen_file = os.path.join(output, f"{prefix}_centres.xyz")
79+
80+
ad_options = jdata.get("AtomicData_options", {})
81+
e_fermi = jdata.get("e_fermi", 0.0)
82+
83+
try:
84+
exporter.write_hr(struct_path, filename=hr_file, AtomicData_options=ad_options, e_fermi=e_fermi)
85+
exporter.write_win(filename=win_file)
86+
exporter.write_centres(filename=cen_file)
87+
except Exception as e:
88+
log.error(f"Error during export: {e}")
89+
90+
elif format.lower() in ["pythtb"]:
91+
try:
92+
exporter = ToPythTB(model, device=device)
93+
except ImportError:
94+
return # Logged in __init__
95+
96+
ad_options = jdata.get("AtomicData_options", {})
97+
e_fermi = jdata.get("e_fermi", 0.0)
98+
99+
try:
100+
tb_model = exporter.get_model(struct_path, AtomicData_options=ad_options, e_fermi=e_fermi)
101+
102+
prefix = os.path.splitext(os.path.basename(struct_path))[0]
103+
pkl_file = os.path.join(output, f"{prefix}_pythtb.pkl")
104+
105+
import pickle
106+
with open(pkl_file, 'wb') as f:
107+
pickle.dump(tb_model, f)
108+
109+
log.info(f"Saved PythTB model object to {pkl_file}")
110+
except Exception as e:
111+
log.error(f"Error during PythTB export: {e}")
112+
113+
else:
114+
log.error(f"Unknown format: {format}")

dptb/entrypoints/main.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
from dptb.utils.loggers import set_log_handles
1414
from dptb.utils.config_check import check_config_train
1515
from dptb.entrypoints.collectskf import skf2pth, skf2nnsk
16+
1617
from dptb.entrypoints.emp_sk import to_empsk
18+
from dptb.entrypoints.export import export
1719

1820
from dptb import __version__
1921

@@ -443,6 +445,54 @@ def main_parser() -> argparse.ArgumentParser:
443445
type=float,
444446
help="Enable SOC, default 0.2 if no value is given. Example: --soc or --soc=0.5"
445447
)
448+
449+
450+
# export parser
451+
parser_export = subparsers.add_parser(
452+
"export",
453+
parents=[parser_log],
454+
help="Export DeePTB model to external formats (Wannier90, PythTB, TB2J)",
455+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
456+
)
457+
458+
parser_export.add_argument(
459+
"INPUT", help="the input parameter file in json format",
460+
type=str,
461+
default=None
462+
)
463+
464+
parser_export.add_argument(
465+
"-i",
466+
"--init-model",
467+
type=str,
468+
default=None,
469+
help="Initialize the model by the provided checkpoint.",
470+
)
471+
472+
parser_export.add_argument(
473+
"-stu",
474+
"--structure",
475+
type=str,
476+
default=None,
477+
help="the structure file name."
478+
)
479+
480+
parser_export.add_argument(
481+
"-o",
482+
"--output",
483+
type=str,
484+
default="./",
485+
help="The output directory."
486+
)
487+
488+
parser_export.add_argument(
489+
"-f",
490+
"--format",
491+
type=str,
492+
default="wannier90",
493+
help="Target format: wannier90 (default) or pythtb."
494+
)
495+
446496
return parser
447497

448498
def parse_args(args: Optional[List[str]] = None) -> argparse.Namespace:
@@ -509,3 +559,6 @@ def main():
509559

510560
elif args.command == 'esk':
511561
to_empsk(**dict_args)
562+
563+
elif args.command == 'export':
564+
export(**dict_args)

0 commit comments

Comments
 (0)