valyte 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- valyte/Logo.png +0 -0
- valyte/__init__.py +0 -0
- valyte/band.py +56 -0
- valyte/band_plot.py +127 -0
- valyte/cli.py +217 -0
- valyte/dos_plot.py +514 -0
- valyte/kpoints.py +96 -0
- valyte/supercell.py +35 -0
- valyte/valyte_band.png +0 -0
- valyte/valyte_dos.png +0 -0
- valyte-0.1.0.dist-info/METADATA +210 -0
- valyte-0.1.0.dist-info/RECORD +15 -0
- valyte-0.1.0.dist-info/WHEEL +5 -0
- valyte-0.1.0.dist-info/entry_points.txt +2 -0
- valyte-0.1.0.dist-info/top_level.txt +1 -0
valyte/Logo.png
ADDED
|
Binary file
|
valyte/__init__.py
ADDED
|
File without changes
|
valyte/band.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Band structure KPOINTS generation module for Valyte.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pymatgen.core import Structure
|
|
7
|
+
from pymatgen.symmetry.bandstructure import HighSymmKpath
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def generate_band_kpoints(poscar_path="POSCAR", npoints=40, output="KPOINTS"):
|
|
11
|
+
"""
|
|
12
|
+
Generates KPOINTS file in line-mode for band structure calculations.
|
|
13
|
+
Uses SeeK-path method for high-symmetry path determination.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
poscar_path (str): Path to input POSCAR file.
|
|
17
|
+
npoints (int): Number of points per segment (default: 40).
|
|
18
|
+
output (str): Output filename for KPOINTS.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
if not os.path.exists(poscar_path):
|
|
22
|
+
raise FileNotFoundError(f"{poscar_path} not found")
|
|
23
|
+
|
|
24
|
+
# Read structure
|
|
25
|
+
structure = Structure.from_file(poscar_path)
|
|
26
|
+
|
|
27
|
+
# Get high-symmetry path using SeeK-path method
|
|
28
|
+
kpath = HighSymmKpath(structure, path_type="setyawan_curtarolo")
|
|
29
|
+
|
|
30
|
+
# Get the path
|
|
31
|
+
path = kpath.kpath["path"]
|
|
32
|
+
kpoints = kpath.kpath["kpoints"]
|
|
33
|
+
|
|
34
|
+
# Write KPOINTS file
|
|
35
|
+
with open(output, 'w') as f:
|
|
36
|
+
f.write("k-points for band structure\n")
|
|
37
|
+
f.write(f"{npoints}\n")
|
|
38
|
+
f.write("Line-mode\n")
|
|
39
|
+
f.write("Reciprocal\n")
|
|
40
|
+
|
|
41
|
+
# Write each segment
|
|
42
|
+
for segment in path:
|
|
43
|
+
for i in range(len(segment) - 1):
|
|
44
|
+
start = segment[i]
|
|
45
|
+
end = segment[i + 1]
|
|
46
|
+
|
|
47
|
+
start_coords = kpoints[start]
|
|
48
|
+
end_coords = kpoints[end]
|
|
49
|
+
|
|
50
|
+
f.write(f" {start_coords[0]:.6f} {start_coords[1]:.6f} {start_coords[2]:.6f} ! {start}\n")
|
|
51
|
+
f.write(f" {end_coords[0]:.6f} {end_coords[1]:.6f} {end_coords[2]:.6f} ! {end}\n")
|
|
52
|
+
f.write("\n")
|
|
53
|
+
|
|
54
|
+
# Print success message
|
|
55
|
+
path_str = ' → '.join([' - '.join(seg) for seg in path])
|
|
56
|
+
print(f"✅ KPOINTS generated: {output} ({path_str}, {npoints} pts/seg)")
|
valyte/band_plot.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
import matplotlib as mpl
|
|
4
|
+
mpl.use("agg")
|
|
5
|
+
mpl.rcParams["axes.unicode_minus"] = False
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
from pymatgen.io.vasp import Vasprun, BSVasprun
|
|
8
|
+
from pymatgen.electronic_structure.plotter import BSPlotter
|
|
9
|
+
|
|
10
|
+
def plot_band_structure(vasprun_path, kpoints_path=None, output="valyte_band.png",
|
|
11
|
+
ylim=None, figsize=(4, 4), dpi=400, font="Arial"):
|
|
12
|
+
"""
|
|
13
|
+
Plots the electronic band structure from vasprun.xml.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
vasprun_path (str): Path to vasprun.xml file.
|
|
17
|
+
kpoints_path (str, optional): Path to KPOINTS file (for labels).
|
|
18
|
+
output (str): Output filename.
|
|
19
|
+
ylim (tuple, optional): Energy range (min, max).
|
|
20
|
+
figsize (tuple): Figure size in inches.
|
|
21
|
+
dpi (int): Resolution of the output image.
|
|
22
|
+
font (str): Font family.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# --- Font configuration ---
|
|
26
|
+
font_map = {
|
|
27
|
+
"arial": "Arial",
|
|
28
|
+
"helvetica": "Helvetica",
|
|
29
|
+
"times": "Times New Roman",
|
|
30
|
+
"times new roman": "Times New Roman",
|
|
31
|
+
}
|
|
32
|
+
font = font_map.get(font.lower(), "Arial")
|
|
33
|
+
mpl.rcParams["font.family"] = font
|
|
34
|
+
mpl.rcParams["axes.linewidth"] = 1.4
|
|
35
|
+
mpl.rcParams["font.weight"] = "bold"
|
|
36
|
+
mpl.rcParams["font.size"] = 14
|
|
37
|
+
mpl.rcParams["xtick.major.width"] = 1.2
|
|
38
|
+
mpl.rcParams["ytick.major.width"] = 1.2
|
|
39
|
+
|
|
40
|
+
# print(f"🔍 Reading {vasprun_path} ...") # Silent mode
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
# Load VASP output
|
|
44
|
+
# BSVasprun is optimized for band structures
|
|
45
|
+
vr = BSVasprun(vasprun_path, parse_projected_eigen=False)
|
|
46
|
+
bs = vr.get_band_structure(kpoints_filename=kpoints_path, line_mode=True)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
raise ValueError(f"Failed to load band structure: {e}")
|
|
49
|
+
|
|
50
|
+
# Use BSPlotter to get the data in a plot-friendly format
|
|
51
|
+
bs_plotter = BSPlotter(bs)
|
|
52
|
+
data = bs_plotter.bs_plot_data(zero_to_efermi=True)
|
|
53
|
+
|
|
54
|
+
# Extract data
|
|
55
|
+
distances = data['distances'] # List of lists (one per segment)
|
|
56
|
+
energies = data['energy'] # Can be list of dicts OR dict of lists depending on pymatgen version/structure
|
|
57
|
+
ticks = data['ticks'] # Dict with 'distance' and 'label'
|
|
58
|
+
|
|
59
|
+
# Setup plot
|
|
60
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
61
|
+
|
|
62
|
+
# Colors
|
|
63
|
+
color_vb = "#8e44ad" # Purple
|
|
64
|
+
color_cb = "#2a9d8f" # Teal
|
|
65
|
+
|
|
66
|
+
# Plot bands
|
|
67
|
+
# Iterate over segments
|
|
68
|
+
for i in range(len(distances)):
|
|
69
|
+
d = distances[i]
|
|
70
|
+
|
|
71
|
+
# Handle different energy data structures
|
|
72
|
+
if isinstance(energies, dict):
|
|
73
|
+
# Structure: {'1': [seg1, seg2, ...], '-1': ...}
|
|
74
|
+
# Iterate over spins
|
|
75
|
+
for spin in energies:
|
|
76
|
+
# energies[spin] is a list of segments
|
|
77
|
+
# energies[spin][i] is the list of bands for segment i
|
|
78
|
+
for band in energies[spin][i]:
|
|
79
|
+
# Determine color based on energy relative to VBM (0 eV)
|
|
80
|
+
if np.mean(band) <= 0:
|
|
81
|
+
c = color_vb
|
|
82
|
+
else:
|
|
83
|
+
c = color_cb
|
|
84
|
+
ax.plot(d, band, color=c, lw=1.5, alpha=1.0)
|
|
85
|
+
else:
|
|
86
|
+
# Structure: [{'1': bands, ...}, {'1': bands, ...}] (List of dicts)
|
|
87
|
+
# Iterate over spin channels in this segment
|
|
88
|
+
for spin in energies[i]:
|
|
89
|
+
# energies[i][spin] is a list of arrays (one per band)
|
|
90
|
+
for band in energies[i][spin]:
|
|
91
|
+
if np.mean(band) <= 0:
|
|
92
|
+
c = color_vb
|
|
93
|
+
else:
|
|
94
|
+
c = color_cb
|
|
95
|
+
ax.plot(d, band, color=c, lw=1.5, alpha=1.0)
|
|
96
|
+
|
|
97
|
+
# Setup X-axis (K-path)
|
|
98
|
+
ax.set_xticks(ticks['distance'])
|
|
99
|
+
# Clean up labels (remove formatting like $ if needed, but pymatgen usually does a good job)
|
|
100
|
+
clean_labels = [l.replace("$\\mid$", "|") for l in ticks['label']]
|
|
101
|
+
ax.set_xticklabels(clean_labels, fontsize=14, fontweight="bold")
|
|
102
|
+
|
|
103
|
+
# Draw vertical lines at high-symmetry points
|
|
104
|
+
for d in ticks['distance']:
|
|
105
|
+
ax.axvline(d, color="k", lw=0.8, ls="-", alpha=0.3)
|
|
106
|
+
|
|
107
|
+
# Draw VBM line (E=0)
|
|
108
|
+
ax.axhline(0, color="k", lw=0.8, ls="--", alpha=0.5)
|
|
109
|
+
|
|
110
|
+
# Setup Y-axis
|
|
111
|
+
ax.set_ylabel("Energy (eV)", fontsize=16, fontweight="bold", labelpad=8)
|
|
112
|
+
if ylim:
|
|
113
|
+
ax.set_ylim(ylim)
|
|
114
|
+
# Set y-ticks with 1 eV spacing
|
|
115
|
+
yticks = np.arange(np.ceil(ylim[0]), np.floor(ylim[1]) + 1, 1)
|
|
116
|
+
ax.set_yticks(yticks)
|
|
117
|
+
else:
|
|
118
|
+
# Default zoom around gap
|
|
119
|
+
ax.set_ylim(-4, 4)
|
|
120
|
+
ax.set_yticks(np.arange(-4, 5, 1))
|
|
121
|
+
|
|
122
|
+
ax.set_xlim(distances[0][0], distances[-1][-1])
|
|
123
|
+
|
|
124
|
+
plt.tight_layout()
|
|
125
|
+
plt.savefig(output, dpi=dpi)
|
|
126
|
+
plt.close(fig)
|
|
127
|
+
# print(f"✅ Band structure saved to {output}") # Silent mode
|
valyte/cli.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Valyte CLI Tool
|
|
4
|
+
===============
|
|
5
|
+
|
|
6
|
+
A post-processing tool for VASP outputs, designed to create publication-quality
|
|
7
|
+
plots with a modern aesthetic. Supports DOS and band structure plotting.
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- DOS plotting with gradient fills
|
|
11
|
+
- Band structure plotting
|
|
12
|
+
- Smart legend positioning
|
|
13
|
+
- Custom font support
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import argparse
|
|
19
|
+
import re
|
|
20
|
+
import warnings
|
|
21
|
+
|
|
22
|
+
# Suppress pymatgen warnings
|
|
23
|
+
warnings.filterwarnings("ignore", category=UserWarning, module="pymatgen")
|
|
24
|
+
|
|
25
|
+
from valyte.supercell import create_supercell
|
|
26
|
+
from valyte.band import generate_band_kpoints
|
|
27
|
+
from valyte.band_plot import plot_band_structure
|
|
28
|
+
from valyte.dos_plot import load_dos, plot_dos
|
|
29
|
+
from valyte.kpoints import generate_kpoints_interactive
|
|
30
|
+
|
|
31
|
+
def parse_element_selection(inputs):
|
|
32
|
+
"""
|
|
33
|
+
Parses user input for elements and orbitals.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
inputs (list): List of strings, e.g., ["Ag", "Bi(s)", "O(p)"]
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
tuple: (elements_to_load, plotting_config)
|
|
40
|
+
elements_to_load (list): Elements to extract from VASP data.
|
|
41
|
+
plotting_config (list): List of (Element, Orbital) tuples.
|
|
42
|
+
"""
|
|
43
|
+
if not inputs:
|
|
44
|
+
return None, None
|
|
45
|
+
|
|
46
|
+
elements_to_load = set()
|
|
47
|
+
plotting_config = []
|
|
48
|
+
|
|
49
|
+
# Regex to match "Element" or "Element(orbital)"
|
|
50
|
+
pattern = re.compile(r"^([A-Za-z]+)(?:\(([spdf])\))?$")
|
|
51
|
+
|
|
52
|
+
for item in inputs:
|
|
53
|
+
match = pattern.match(item)
|
|
54
|
+
if match:
|
|
55
|
+
el = match.group(1)
|
|
56
|
+
orb = match.group(2) # None if no orbital specified
|
|
57
|
+
|
|
58
|
+
elements_to_load.add(el)
|
|
59
|
+
if orb:
|
|
60
|
+
plotting_config.append((el, orb))
|
|
61
|
+
else:
|
|
62
|
+
plotting_config.append((el, 'total'))
|
|
63
|
+
else:
|
|
64
|
+
print(f"⚠️ Warning: Could not parse '{item}'. Ignoring.")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
return list(elements_to_load), plotting_config
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ===============================================================
|
|
71
|
+
# Main CLI
|
|
72
|
+
# ===============================================================
|
|
73
|
+
def main():
|
|
74
|
+
"""
|
|
75
|
+
Main entry point for the CLI.
|
|
76
|
+
"""
|
|
77
|
+
parser = argparse.ArgumentParser(description="Valyte: VASP Post-Processing Tool")
|
|
78
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
79
|
+
|
|
80
|
+
# --- DOS Subcommand ---
|
|
81
|
+
dos_parser = subparsers.add_parser("dos", help="Plot Density of States (DOS)")
|
|
82
|
+
dos_parser.add_argument("filepath", nargs="?", help="Path to vasprun.xml or directory containing it (optional)")
|
|
83
|
+
dos_parser.add_argument("--vasprun", help="Explicit path to vasprun.xml (alternative to positional argument)")
|
|
84
|
+
dos_parser.add_argument("-e", "--elements", nargs="+", help="Elements/Orbitals to plot (e.g., 'Fe O' or 'Fe(d) O(p)')")
|
|
85
|
+
dos_parser.add_argument("-o", "--output", default="valyte_dos.png", help="Output filename")
|
|
86
|
+
dos_parser.add_argument("--xlim", nargs=2, type=float, default=[-6, 6], help="Energy range (min max)")
|
|
87
|
+
dos_parser.add_argument("--ylim", nargs=2, type=float, help="DOS range (min max)")
|
|
88
|
+
dos_parser.add_argument("--scale", type=float, default=1.0, help="Scaling factor for Y-axis (zoom in)")
|
|
89
|
+
dos_parser.add_argument("--fermi", action="store_true", help="Draw dashed line at Fermi level (E=0)")
|
|
90
|
+
dos_parser.add_argument("--pdos", action="store_true", help="Plot only Projected DOS (hide Total DOS)")
|
|
91
|
+
dos_parser.add_argument("--legend-cutoff", type=float, default=0.10, help="Threshold for legend visibility (0.0-1.0)")
|
|
92
|
+
dos_parser.add_argument("--font", default="Arial", help="Font family")
|
|
93
|
+
|
|
94
|
+
# --- Supercell Subcommand ---
|
|
95
|
+
supercell_parser = subparsers.add_parser("supercell", help="Create a supercell")
|
|
96
|
+
supercell_parser.add_argument("nx", type=int, help="Supercell size x")
|
|
97
|
+
supercell_parser.add_argument("ny", type=int, help="Supercell size y")
|
|
98
|
+
supercell_parser.add_argument("nz", type=int, help="Supercell size z")
|
|
99
|
+
supercell_parser.add_argument("-i", "--input", default="POSCAR", help="Input POSCAR file")
|
|
100
|
+
supercell_parser.add_argument("-o", "--output", default="POSCAR_supercell", help="Output filename")
|
|
101
|
+
|
|
102
|
+
# --- Band Structure Subcommand ---
|
|
103
|
+
band_parser = subparsers.add_parser("band", help="Band structure utilities")
|
|
104
|
+
band_subparsers = band_parser.add_subparsers(dest="band_command", help="Band commands")
|
|
105
|
+
|
|
106
|
+
# Band Plotting (default if no subcommand)
|
|
107
|
+
# Note: argparse doesn't easily support default subcommands, so we handle this in logic
|
|
108
|
+
band_parser.add_argument("--vasprun", default=".", help="Path to vasprun.xml or directory")
|
|
109
|
+
band_parser.add_argument("--kpoints", help="Path to KPOINTS file (for labels)")
|
|
110
|
+
band_parser.add_argument("-o", "--output", default="valyte_band.png", help="Output filename")
|
|
111
|
+
band_parser.add_argument("--ylim", nargs=2, type=float, help="Energy range (min max)")
|
|
112
|
+
band_parser.add_argument("--font", default="Arial", help="Font family")
|
|
113
|
+
|
|
114
|
+
# Band KPOINTS Generation
|
|
115
|
+
kpt_gen_parser = band_subparsers.add_parser("kpt-gen", help="Generate KPOINTS for band structure")
|
|
116
|
+
kpt_gen_parser.add_argument("-i", "--input", default="POSCAR", help="Input POSCAR file")
|
|
117
|
+
kpt_gen_parser.add_argument("-n", "--npoints", type=int, default=40, help="Points per segment")
|
|
118
|
+
kpt_gen_parser.add_argument("-o", "--output", default="KPOINTS", help="Output filename")
|
|
119
|
+
|
|
120
|
+
# --- K-Point Generation (Interactive) ---
|
|
121
|
+
subparsers.add_parser("kpt", help="Interactive K-Point Generation (SCF)")
|
|
122
|
+
|
|
123
|
+
args = parser.parse_args()
|
|
124
|
+
|
|
125
|
+
if args.command == "dos":
|
|
126
|
+
# Resolve filepath: positional > flag > current dir
|
|
127
|
+
target_path = args.filepath if args.filepath else args.vasprun
|
|
128
|
+
if not target_path:
|
|
129
|
+
target_path = "."
|
|
130
|
+
|
|
131
|
+
elements, plotting_config = parse_element_selection(args.elements)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
dos_data, pdos_data = load_dos(target_path, elements)
|
|
135
|
+
plot_dos(
|
|
136
|
+
dos_data, pdos_data,
|
|
137
|
+
out=args.output,
|
|
138
|
+
xlim=tuple(args.xlim),
|
|
139
|
+
ylim=tuple(args.ylim) if args.ylim else None,
|
|
140
|
+
font=args.font,
|
|
141
|
+
show_fermi=args.fermi,
|
|
142
|
+
show_total=not args.pdos,
|
|
143
|
+
plotting_config=plotting_config,
|
|
144
|
+
legend_cutoff=args.legend_cutoff,
|
|
145
|
+
scale_factor=args.scale
|
|
146
|
+
)
|
|
147
|
+
# print(f"✅ DOS plot saved to {args.output}") # Silent mode
|
|
148
|
+
except Exception as e:
|
|
149
|
+
print(f"❌ Error: {e}")
|
|
150
|
+
sys.exit(1)
|
|
151
|
+
|
|
152
|
+
elif args.command == "supercell":
|
|
153
|
+
try:
|
|
154
|
+
create_supercell(
|
|
155
|
+
poscar_path=args.input,
|
|
156
|
+
nx=args.nx,
|
|
157
|
+
ny=args.ny,
|
|
158
|
+
nz=args.nz,
|
|
159
|
+
output=args.output
|
|
160
|
+
)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
print(f"❌ Error: {e}")
|
|
163
|
+
sys.exit(1)
|
|
164
|
+
|
|
165
|
+
elif args.command == "kpt":
|
|
166
|
+
try:
|
|
167
|
+
generate_kpoints_interactive()
|
|
168
|
+
except Exception as e:
|
|
169
|
+
print(f"❌ Error: {e}")
|
|
170
|
+
sys.exit(1)
|
|
171
|
+
|
|
172
|
+
elif args.command == "band":
|
|
173
|
+
if args.band_command == "kpt-gen":
|
|
174
|
+
try:
|
|
175
|
+
generate_band_kpoints(
|
|
176
|
+
poscar_path=args.input,
|
|
177
|
+
npoints=args.npoints,
|
|
178
|
+
output=args.output
|
|
179
|
+
)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
print(f"❌ Error: {e}")
|
|
182
|
+
sys.exit(1)
|
|
183
|
+
elif args.band_command == "plot" or args.band_command is None:
|
|
184
|
+
# Default behavior for 'valyte band' is plotting
|
|
185
|
+
try:
|
|
186
|
+
# Determine input path: --vasprun > positional > current dir
|
|
187
|
+
target_path = args.vasprun or args.filepath or "."
|
|
188
|
+
if os.path.isdir(target_path):
|
|
189
|
+
target_path = os.path.join(target_path, "vasprun.xml")
|
|
190
|
+
|
|
191
|
+
# Determine KPOINTS path
|
|
192
|
+
kpoints_path = args.kpoints
|
|
193
|
+
if not kpoints_path:
|
|
194
|
+
# Try to find KPOINTS in the same directory as vasprun.xml
|
|
195
|
+
base_dir = os.path.dirname(target_path)
|
|
196
|
+
potential_kpoints = os.path.join(base_dir, "KPOINTS")
|
|
197
|
+
if os.path.exists(potential_kpoints):
|
|
198
|
+
kpoints_path = potential_kpoints
|
|
199
|
+
|
|
200
|
+
plot_band_structure(
|
|
201
|
+
vasprun_path=target_path,
|
|
202
|
+
kpoints_path=kpoints_path,
|
|
203
|
+
output=args.output,
|
|
204
|
+
ylim=tuple(args.ylim) if args.ylim else None,
|
|
205
|
+
font=args.font
|
|
206
|
+
)
|
|
207
|
+
except Exception:
|
|
208
|
+
import traceback
|
|
209
|
+
traceback.print_exc()
|
|
210
|
+
sys.exit(1)
|
|
211
|
+
else:
|
|
212
|
+
band_parser.print_help()
|
|
213
|
+
else:
|
|
214
|
+
parser.print_help()
|
|
215
|
+
|
|
216
|
+
if __name__ == "__main__":
|
|
217
|
+
main()
|
valyte/dos_plot.py
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
DOS Plotting Module
|
|
4
|
+
===================
|
|
5
|
+
|
|
6
|
+
Handles Density of States (DOS) plotting with gradient fills and smart legend.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import numpy as np
|
|
11
|
+
import matplotlib as mpl
|
|
12
|
+
mpl.use("agg")
|
|
13
|
+
mpl.rcParams["axes.unicode_minus"] = False
|
|
14
|
+
import matplotlib.pyplot as plt
|
|
15
|
+
import matplotlib.colors as mcolors
|
|
16
|
+
from matplotlib.patches import Polygon
|
|
17
|
+
from matplotlib.ticker import AutoMinorLocator
|
|
18
|
+
from pymatgen.io.vasp import Vasprun
|
|
19
|
+
from pymatgen.electronic_structure.core import Spin
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ===============================================================
|
|
23
|
+
# Gradient fill aesthetic
|
|
24
|
+
# ===============================================================
|
|
25
|
+
def gradient_fill(x, y, ax=None, color=None, xlim=None, **kwargs):
|
|
26
|
+
"""
|
|
27
|
+
Fills the area under a curve with a vertical gradient.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
x (array-like): X-axis data (Energy).
|
|
31
|
+
y (array-like): Y-axis data (DOS).
|
|
32
|
+
ax (matplotlib.axes.Axes, optional): The axes to plot on. Defaults to current axes.
|
|
33
|
+
color (str, optional): The base color for the gradient.
|
|
34
|
+
xlim (tuple, optional): X-axis limits to restrict gradient fill.
|
|
35
|
+
**kwargs: Additional arguments passed to ax.plot.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
matplotlib.lines.Line2D: The line object representing the curve.
|
|
39
|
+
"""
|
|
40
|
+
if ax is None:
|
|
41
|
+
ax = plt.gca()
|
|
42
|
+
|
|
43
|
+
# Don't filter by xlim - use full data range for better appearance
|
|
44
|
+
if len(x) == 0 or len(y) == 0:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
# Plot the main line
|
|
48
|
+
line, = ax.plot(x, y, color=color, lw=2, **kwargs)
|
|
49
|
+
|
|
50
|
+
# Determine fill color and alpha
|
|
51
|
+
fill_color = line.get_color() if color is None else color
|
|
52
|
+
alpha = line.get_alpha() or 1.0
|
|
53
|
+
zorder = line.get_zorder()
|
|
54
|
+
|
|
55
|
+
# Create a gradient image with more aggressive alpha
|
|
56
|
+
z = np.empty((100, 1, 4))
|
|
57
|
+
rgb = mcolors.to_rgb(fill_color)
|
|
58
|
+
z[:, :, :3] = rgb
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Gradient: transparent at bottom (y=0), opaque near the curve
|
|
62
|
+
# This creates a gradient from bottom to top of the filled area
|
|
63
|
+
alpha_gradient = np.linspace(0.05, 0.75, 100)
|
|
64
|
+
|
|
65
|
+
# If data is negative (Spin Down), we want opaque at bottom (peak) and transparent at top (axis)
|
|
66
|
+
# Current extent is [ymin, ymax]. ymin is bottom. ymax is top (0).
|
|
67
|
+
# So we want High Alpha at index 0 and Low Alpha at index 100.
|
|
68
|
+
if np.mean(y) < 0:
|
|
69
|
+
alpha_gradient = alpha_gradient[::-1]
|
|
70
|
+
|
|
71
|
+
z[:, :, -1] = alpha_gradient[:, None]
|
|
72
|
+
|
|
73
|
+
xmin, xmax = x.min(), x.max()
|
|
74
|
+
ymin, ymax = min(y.min(), 0), max(y.max(), 0)
|
|
75
|
+
|
|
76
|
+
# Handle pure negative or pure positive cases to avoid singular extent
|
|
77
|
+
if ymax == ymin:
|
|
78
|
+
ymax += 1e-6
|
|
79
|
+
|
|
80
|
+
# Display the gradient image
|
|
81
|
+
im = ax.imshow(z, aspect="auto", extent=[xmin, xmax, ymin, ymax],
|
|
82
|
+
origin="lower", zorder=zorder)
|
|
83
|
+
|
|
84
|
+
# Clip the gradient to the area under the curve
|
|
85
|
+
# We need to close the polygon at y=0
|
|
86
|
+
xy = np.column_stack([x, y])
|
|
87
|
+
|
|
88
|
+
# Construct polygon vertices:
|
|
89
|
+
# Start at (xmin, 0), go along curve (x, y), end at (xmax, 0), close back to start
|
|
90
|
+
verts = np.vstack([[x[0], 0], xy, [x[-1], 0], [x[0], 0]])
|
|
91
|
+
|
|
92
|
+
clip = Polygon(verts, lw=0, facecolor="none", closed=True)
|
|
93
|
+
ax.add_patch(clip)
|
|
94
|
+
im.set_clip_path(clip)
|
|
95
|
+
|
|
96
|
+
return line
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ===============================================================
|
|
100
|
+
# Data container
|
|
101
|
+
# ===============================================================
|
|
102
|
+
class ValyteDos:
|
|
103
|
+
"""
|
|
104
|
+
Container for Total DOS data.
|
|
105
|
+
|
|
106
|
+
Attributes:
|
|
107
|
+
energies (np.ndarray): Array of energy values (shifted by Fermi energy).
|
|
108
|
+
densities (dict): Dictionary of {Spin: np.ndarray} for total DOS.
|
|
109
|
+
efermi (float): Fermi energy.
|
|
110
|
+
"""
|
|
111
|
+
def __init__(self, energies, densities, efermi):
|
|
112
|
+
self.energies = np.array(energies)
|
|
113
|
+
self.densities = densities
|
|
114
|
+
self.efermi = float(efermi)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def total(self):
|
|
118
|
+
"""Returns the sum of all spin channels."""
|
|
119
|
+
tot = np.zeros_like(self.energies)
|
|
120
|
+
for spin in self.densities:
|
|
121
|
+
tot += self.densities[spin]
|
|
122
|
+
return tot
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def spin_up(self):
|
|
126
|
+
return self.densities.get(Spin.up, np.zeros_like(self.energies))
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def spin_down(self):
|
|
130
|
+
return self.densities.get(Spin.down, np.zeros_like(self.energies))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ===============================================================
|
|
134
|
+
# Load DOS
|
|
135
|
+
# ===============================================================
|
|
136
|
+
def load_dos(vasprun, elements=None, **_):
|
|
137
|
+
"""
|
|
138
|
+
Loads DOS data from a vasprun.xml file using pymatgen.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
vasprun (str): Path to the vasprun.xml file or directory containing it.
|
|
142
|
+
elements (list or dict, optional): Specific elements to extract PDOS for.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
tuple: (ValyteDos object, dict of PDOS data)
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
# Handle directory input
|
|
149
|
+
if os.path.isdir(vasprun):
|
|
150
|
+
vasprun = os.path.join(vasprun, "vasprun.xml")
|
|
151
|
+
|
|
152
|
+
if not os.path.exists(vasprun):
|
|
153
|
+
raise FileNotFoundError(f"{vasprun} not found")
|
|
154
|
+
|
|
155
|
+
# Parse VASP output
|
|
156
|
+
vr = Vasprun(vasprun)
|
|
157
|
+
dos = vr.complete_dos
|
|
158
|
+
|
|
159
|
+
# Get Fermi Energy
|
|
160
|
+
efermi = dos.efermi
|
|
161
|
+
|
|
162
|
+
# Attempt to align VBM to 0 for insulators/semiconductors
|
|
163
|
+
try:
|
|
164
|
+
# Try using BandStructure first (more robust)
|
|
165
|
+
bs = vr.get_band_structure()
|
|
166
|
+
if not bs.is_metal():
|
|
167
|
+
efermi = bs.get_vbm()["energy"]
|
|
168
|
+
except Exception:
|
|
169
|
+
# Fallback to DOS-based detection
|
|
170
|
+
try:
|
|
171
|
+
cbm, vbm = dos.get_cbm_vbm()
|
|
172
|
+
if cbm - vbm > 0.01: # Band gap detected
|
|
173
|
+
efermi = vbm
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
# Shift energies to set reference at 0
|
|
178
|
+
energies = dos.energies - efermi
|
|
179
|
+
|
|
180
|
+
# Extract Projected DOS
|
|
181
|
+
pdos = get_pdos(dos, elements)
|
|
182
|
+
|
|
183
|
+
return ValyteDos(energies, dos.densities, efermi), pdos
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ===============================================================
|
|
187
|
+
# Extract PDOS
|
|
188
|
+
# ===============================================================
|
|
189
|
+
def get_pdos(dos, elements=None):
|
|
190
|
+
"""
|
|
191
|
+
Extracts Projected DOS (PDOS) for specified elements.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
dos (pymatgen.electronic_structure.dos.CompleteDos): The complete DOS object.
|
|
195
|
+
elements (list or dict, optional): Elements to extract. If None, extracts all.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
dict: A dictionary where keys are element symbols and values are dicts of orbital DOS.
|
|
199
|
+
pdos[element][orbital] = {Spin.up: array, Spin.down: array}
|
|
200
|
+
"""
|
|
201
|
+
structure = dos.structure
|
|
202
|
+
symbols = [str(site.specie) for site in structure]
|
|
203
|
+
|
|
204
|
+
# If no elements specified, use all unique elements in the structure
|
|
205
|
+
if not elements:
|
|
206
|
+
unique = sorted(set(symbols))
|
|
207
|
+
elements = {el: () for el in unique}
|
|
208
|
+
else:
|
|
209
|
+
# Ensure elements is a dict if passed as list
|
|
210
|
+
if isinstance(elements, list):
|
|
211
|
+
elements = {el: () for el in elements}
|
|
212
|
+
|
|
213
|
+
pdos = {}
|
|
214
|
+
for el in elements:
|
|
215
|
+
# Find all sites corresponding to this element
|
|
216
|
+
el_sites = [s for s in structure if str(s.specie) == el]
|
|
217
|
+
el_pdos = {}
|
|
218
|
+
|
|
219
|
+
for site in el_sites:
|
|
220
|
+
try:
|
|
221
|
+
site_dos = dos.get_site_spd_dos(site)
|
|
222
|
+
except Exception:
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
# Sum up contributions from orbitals (s, p, d, f)
|
|
226
|
+
for orb, orb_dos in site_dos.items():
|
|
227
|
+
label = orb.name[0] # e.g., 's', 'p', 'd'
|
|
228
|
+
|
|
229
|
+
# Initialize dictionaries for spins if not exists
|
|
230
|
+
if label not in el_pdos:
|
|
231
|
+
el_pdos[label] = {}
|
|
232
|
+
|
|
233
|
+
for spin in orb_dos.densities:
|
|
234
|
+
if spin not in el_pdos[label]:
|
|
235
|
+
el_pdos[label][spin] = np.zeros_like(dos.energies)
|
|
236
|
+
el_pdos[label][spin] += orb_dos.densities[spin]
|
|
237
|
+
|
|
238
|
+
pdos[el] = el_pdos
|
|
239
|
+
return pdos
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ===============================================================
|
|
243
|
+
# Plotting with smart legend & font control (Valyte theme)
|
|
244
|
+
# ===============================================================
|
|
245
|
+
def plot_dos(dos, pdos, out="valyte_dos.png",
|
|
246
|
+
xlim=(-6, 6), ylim=None, figsize=(5, 4),
|
|
247
|
+
dpi=400, legend_loc="auto", font="Arial",
|
|
248
|
+
show_fermi=False, show_total=True, plotting_config=None,
|
|
249
|
+
legend_cutoff=0.10, scale_factor=1.0):
|
|
250
|
+
"""
|
|
251
|
+
Plots the Total and Projected DOS with the Valyte visual style.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
dos (ValyteDos): The total DOS data.
|
|
255
|
+
pdos (dict): The projected DOS data.
|
|
256
|
+
out (str): Output filename.
|
|
257
|
+
xlim (tuple): Energy range (min, max).
|
|
258
|
+
ylim (tuple, optional): DOS range (min, max).
|
|
259
|
+
figsize (tuple): Figure size in inches.
|
|
260
|
+
dpi (int): Resolution of the output image.
|
|
261
|
+
legend_loc (str): Legend location strategy.
|
|
262
|
+
font (str): Font family to use.
|
|
263
|
+
show_fermi (bool): Whether to draw a dashed line at the Fermi level (E=0).
|
|
264
|
+
show_total (bool): Whether to plot the Total DOS.
|
|
265
|
+
plotting_config (list): List of (Element, Orbital) tuples to plot.
|
|
266
|
+
legend_cutoff (float): Threshold (as fraction) for showing items in legend (default: 0.10).
|
|
267
|
+
scale_factor (float): Factor to scale the Y-axis limits (zoom in).
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
# --- Font configuration ---
|
|
271
|
+
font_map = {
|
|
272
|
+
"arial": "Arial",
|
|
273
|
+
"helvetica": "Helvetica",
|
|
274
|
+
"times": "Times New Roman",
|
|
275
|
+
"times new roman": "Times New Roman",
|
|
276
|
+
}
|
|
277
|
+
font = font_map.get(font.lower(), "Arial")
|
|
278
|
+
mpl.rcParams["font.family"] = font
|
|
279
|
+
mpl.rcParams["axes.linewidth"] = 1.4
|
|
280
|
+
mpl.rcParams["font.weight"] = "bold"
|
|
281
|
+
mpl.rcParams["font.size"] = 12
|
|
282
|
+
|
|
283
|
+
plt.style.use("default")
|
|
284
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
285
|
+
|
|
286
|
+
# Check if spin-polarized
|
|
287
|
+
is_spin_polarized = Spin.down in dos.densities
|
|
288
|
+
|
|
289
|
+
# Fermi level line (optional)
|
|
290
|
+
if show_fermi:
|
|
291
|
+
ax.axvline(0, color="k", lw=0.8, ls="--", alpha=0.7)
|
|
292
|
+
|
|
293
|
+
# Zero line for spin polarized plots
|
|
294
|
+
if is_spin_polarized:
|
|
295
|
+
ax.axhline(0, color="k", lw=0.5, alpha=1.0)
|
|
296
|
+
|
|
297
|
+
# Color palette for elements
|
|
298
|
+
# Expanded color palette for better distinction
|
|
299
|
+
# Reordered to maximize contrast between consecutive items
|
|
300
|
+
palette = [
|
|
301
|
+
"#4b0082", # Indigo
|
|
302
|
+
"#e63946", # Red
|
|
303
|
+
"#2a9d8f", # Teal
|
|
304
|
+
"#ffb703", # Yellow
|
|
305
|
+
"#0077b6", # Blue
|
|
306
|
+
"#8e44ad", # Purple
|
|
307
|
+
"#d62828", # Dark Red
|
|
308
|
+
"#118ab2", # Light Blue
|
|
309
|
+
"#f4a261", # Orange
|
|
310
|
+
"#003049", # Dark Blue
|
|
311
|
+
"#6a994e", # Green
|
|
312
|
+
"#023e8a", # Royal Blue
|
|
313
|
+
"#0096c7", # Cyan
|
|
314
|
+
"#00b4d8", # Sky Blue
|
|
315
|
+
"#48cae4", # Light Cyan
|
|
316
|
+
"#90e0ef", # Pale Blue
|
|
317
|
+
"#ade8f4", # Very Pale Blue
|
|
318
|
+
"#caf0f8" # White Blue
|
|
319
|
+
]
|
|
320
|
+
lines, labels = [], []
|
|
321
|
+
|
|
322
|
+
# Determine mask for visible x-range (used for scaling and legend)
|
|
323
|
+
x_mask = (dos.energies >= xlim[0]) & (dos.energies <= xlim[1])
|
|
324
|
+
|
|
325
|
+
# Determine what to plot
|
|
326
|
+
if plotting_config:
|
|
327
|
+
items_to_plot = plotting_config
|
|
328
|
+
else:
|
|
329
|
+
# Default: Plot all orbitals for each loaded element
|
|
330
|
+
items_to_plot = []
|
|
331
|
+
for el, el_pdos in pdos.items():
|
|
332
|
+
for orb in el_pdos.keys():
|
|
333
|
+
items_to_plot.append((el, orb))
|
|
334
|
+
|
|
335
|
+
# Plot PDOS
|
|
336
|
+
max_visible_y = 0 # Track maximum Y value in visible range
|
|
337
|
+
min_visible_y = 0 # Track minimum Y value (for spin down)
|
|
338
|
+
|
|
339
|
+
for i, (el, orb) in enumerate(items_to_plot):
|
|
340
|
+
if el not in pdos:
|
|
341
|
+
continue
|
|
342
|
+
|
|
343
|
+
# Assign unique color for each orbital contribution
|
|
344
|
+
c = palette[i % len(palette)]
|
|
345
|
+
|
|
346
|
+
# Prepare data for plotting
|
|
347
|
+
# y_data_up and y_data_down
|
|
348
|
+
|
|
349
|
+
if orb == 'total':
|
|
350
|
+
# Sum all orbitals for this element
|
|
351
|
+
y_up = np.zeros_like(dos.energies)
|
|
352
|
+
y_down = np.zeros_like(dos.energies)
|
|
353
|
+
|
|
354
|
+
for o_data in pdos[el].values():
|
|
355
|
+
y_up += o_data.get(Spin.up, np.zeros_like(dos.energies))
|
|
356
|
+
y_down += o_data.get(Spin.down, np.zeros_like(dos.energies))
|
|
357
|
+
|
|
358
|
+
label = el
|
|
359
|
+
else:
|
|
360
|
+
if orb in pdos[el]:
|
|
361
|
+
y_up = pdos[el][orb].get(Spin.up, np.zeros_like(dos.energies))
|
|
362
|
+
y_down = pdos[el][orb].get(Spin.down, np.zeros_like(dos.energies))
|
|
363
|
+
label = f"{el}({orb})"
|
|
364
|
+
else:
|
|
365
|
+
continue
|
|
366
|
+
|
|
367
|
+
# Invert spin down
|
|
368
|
+
y_down = -y_down
|
|
369
|
+
|
|
370
|
+
# Check contribution in visible range
|
|
371
|
+
visible_y_up = y_up[x_mask]
|
|
372
|
+
visible_y_down = y_down[x_mask]
|
|
373
|
+
|
|
374
|
+
has_visible_data = False
|
|
375
|
+
current_max_y = 0
|
|
376
|
+
|
|
377
|
+
if len(visible_y_up) > 0:
|
|
378
|
+
max_y = np.max(visible_y_up)
|
|
379
|
+
max_visible_y = max(max_visible_y, max_y)
|
|
380
|
+
current_max_y = max(current_max_y, max_y)
|
|
381
|
+
if max_y > 1e-6: has_visible_data = True
|
|
382
|
+
|
|
383
|
+
if is_spin_polarized and len(visible_y_down) > 0:
|
|
384
|
+
min_y = np.min(visible_y_down)
|
|
385
|
+
min_visible_y = min(min_visible_y, min_y)
|
|
386
|
+
current_max_y = max(current_max_y, abs(min_y))
|
|
387
|
+
if abs(min_y) > 1e-6: has_visible_data = True
|
|
388
|
+
|
|
389
|
+
# Store for later threshold check and plotting
|
|
390
|
+
# We plot a dummy line for the legend
|
|
391
|
+
line, = ax.plot(dos.energies, y_up, lw=1.5, color=c, label=label, alpha=0)
|
|
392
|
+
lines.append({
|
|
393
|
+
'line': line,
|
|
394
|
+
'y_up': y_up,
|
|
395
|
+
'y_down': y_down,
|
|
396
|
+
'max_y': current_max_y,
|
|
397
|
+
'color': c,
|
|
398
|
+
'label': label
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
# Calculate threshold (legend_cutoff of max visible)
|
|
402
|
+
# Use the overall max absolute value found
|
|
403
|
+
global_max = max(max_visible_y, abs(min_visible_y))
|
|
404
|
+
threshold = legend_cutoff * global_max
|
|
405
|
+
|
|
406
|
+
# Filter legend items but keep all plot lines
|
|
407
|
+
final_lines = []
|
|
408
|
+
final_labels = []
|
|
409
|
+
|
|
410
|
+
for item in lines:
|
|
411
|
+
line = item['line']
|
|
412
|
+
y_up = item['y_up']
|
|
413
|
+
y_down = item['y_down']
|
|
414
|
+
c = item['color']
|
|
415
|
+
label = item['label']
|
|
416
|
+
max_y = item['max_y']
|
|
417
|
+
|
|
418
|
+
# Always plot the line (make it visible)
|
|
419
|
+
line.set_alpha(1.0)
|
|
420
|
+
|
|
421
|
+
# Apply gradient fill for visible lines
|
|
422
|
+
# Spin Up
|
|
423
|
+
gradient_fill(dos.energies, y_up, ax=ax, color=c, alpha=0.9)
|
|
424
|
+
|
|
425
|
+
# Spin Down
|
|
426
|
+
if is_spin_polarized:
|
|
427
|
+
gradient_fill(dos.energies, y_down, ax=ax, color=c, alpha=0.9)
|
|
428
|
+
|
|
429
|
+
# Only add to legend if above main threshold
|
|
430
|
+
if max_y >= threshold:
|
|
431
|
+
final_lines.append(line)
|
|
432
|
+
final_labels.append(label)
|
|
433
|
+
|
|
434
|
+
# Update lines and labels for legend creation
|
|
435
|
+
lines = final_lines
|
|
436
|
+
labels = final_labels
|
|
437
|
+
|
|
438
|
+
# Plot Total DOS
|
|
439
|
+
if show_total:
|
|
440
|
+
y_total_up = dos.spin_up
|
|
441
|
+
y_total_down = -dos.spin_down
|
|
442
|
+
|
|
443
|
+
ax.plot(dos.energies, y_total_up, color="k", lw=1.2, label="Total DOS")
|
|
444
|
+
gradient_fill(dos.energies, y_total_up, ax=ax, color="k", alpha=0.15)
|
|
445
|
+
|
|
446
|
+
if is_spin_polarized:
|
|
447
|
+
ax.plot(dos.energies, y_total_down, color="k", lw=1.2)
|
|
448
|
+
gradient_fill(dos.energies, y_total_down, ax=ax, color="k", alpha=0.15)
|
|
449
|
+
|
|
450
|
+
# Update max/min range for auto-scaling
|
|
451
|
+
visible_total_up = y_total_up[x_mask]
|
|
452
|
+
visible_total_down = y_total_down[x_mask]
|
|
453
|
+
if len(visible_total_up) > 0:
|
|
454
|
+
max_visible_y = max(max_visible_y, np.max(visible_total_up))
|
|
455
|
+
if len(visible_total_down) > 0:
|
|
456
|
+
min_visible_y = min(min_visible_y, np.min(visible_total_down))
|
|
457
|
+
|
|
458
|
+
# Auto-scale Y-axis based on visible range if ylim not provided
|
|
459
|
+
if not ylim:
|
|
460
|
+
if max_visible_y > 0 or min_visible_y < 0:
|
|
461
|
+
# Apply scaling factor to the limit (zoom in)
|
|
462
|
+
upper_limit = (max_visible_y * 1.1) / scale_factor
|
|
463
|
+
lower_limit = (min_visible_y * 1.1) / scale_factor if is_spin_polarized else 0
|
|
464
|
+
|
|
465
|
+
# If spin polarized, maybe make symmetric if user wants?
|
|
466
|
+
# For now, let's just use the data range.
|
|
467
|
+
# But often symmetric is nicer. Let's stick to data range for now.
|
|
468
|
+
|
|
469
|
+
ax.set_ylim(lower_limit, upper_limit)
|
|
470
|
+
else:
|
|
471
|
+
ax.set_ylim(*ylim)
|
|
472
|
+
|
|
473
|
+
# Axis settings
|
|
474
|
+
ax.set_xlim(*xlim)
|
|
475
|
+
ax.set_xlabel("Energy (eV)", fontsize=14, weight="bold", labelpad=6)
|
|
476
|
+
ax.set_ylabel("Density of States", fontsize=14, weight="bold", labelpad=6)
|
|
477
|
+
|
|
478
|
+
# Set x-ticks with 1 eV spacing
|
|
479
|
+
xticks = np.arange(np.ceil(xlim[0]), np.floor(xlim[1]) + 1, 1)
|
|
480
|
+
ax.set_xticks(xticks)
|
|
481
|
+
# Format tick labels: show integers without .0
|
|
482
|
+
tick_labels = [f'{int(x)}' if x == int(x) else f'{x}' for x in xticks]
|
|
483
|
+
ax.set_xticklabels(tick_labels, fontweight="bold")
|
|
484
|
+
ax.set_yticks([])
|
|
485
|
+
|
|
486
|
+
# --- Smart legend visibility ---
|
|
487
|
+
# Only show legend if there are items to display
|
|
488
|
+
show_legend = len(lines) > 0
|
|
489
|
+
|
|
490
|
+
if show_legend:
|
|
491
|
+
# Check for overlap to decide legend position
|
|
492
|
+
# Simplified overlap check for now
|
|
493
|
+
loc, ncol = "upper right", 1
|
|
494
|
+
|
|
495
|
+
# If spin polarized, upper right might cover spin up data
|
|
496
|
+
# But usually it's fine.
|
|
497
|
+
|
|
498
|
+
legend = ax.legend(
|
|
499
|
+
lines, labels,
|
|
500
|
+
frameon=False,
|
|
501
|
+
fontsize=13,
|
|
502
|
+
loc=loc,
|
|
503
|
+
ncol=ncol,
|
|
504
|
+
handlelength=1.5,
|
|
505
|
+
columnspacing=0.8,
|
|
506
|
+
handletextpad=0.6,
|
|
507
|
+
)
|
|
508
|
+
for text in legend.get_texts():
|
|
509
|
+
text.set_fontweight("bold")
|
|
510
|
+
|
|
511
|
+
ax.xaxis.set_minor_locator(AutoMinorLocator(2))
|
|
512
|
+
plt.tight_layout(pad=0.4)
|
|
513
|
+
plt.savefig(out, dpi=dpi)
|
|
514
|
+
plt.close(fig)
|
valyte/kpoints.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive K-Point Generation Module
|
|
3
|
+
=====================================
|
|
4
|
+
|
|
5
|
+
Handles interactive generation of KPOINTS files based on user input and structure.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import numpy as np
|
|
11
|
+
from pymatgen.core import Structure
|
|
12
|
+
from pymatgen.io.vasp.inputs import Kpoints
|
|
13
|
+
|
|
14
|
+
def generate_kpoints_interactive():
|
|
15
|
+
"""
|
|
16
|
+
Interactively generates a KPOINTS file based on user input and POSCAR.
|
|
17
|
+
"""
|
|
18
|
+
print("\n🔮 Valyte K-Point Generator")
|
|
19
|
+
|
|
20
|
+
# Check for POSCAR
|
|
21
|
+
poscar_path = "POSCAR"
|
|
22
|
+
if not os.path.exists(poscar_path):
|
|
23
|
+
# Try finding any POSCAR* file
|
|
24
|
+
files = [f for f in os.listdir('.') if f.startswith('POSCAR')]
|
|
25
|
+
if files:
|
|
26
|
+
poscar_path = files[0]
|
|
27
|
+
print(f" Found structure: {poscar_path}")
|
|
28
|
+
else:
|
|
29
|
+
print("❌ POSCAR file not found in current directory.")
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
structure = Structure.from_file(poscar_path)
|
|
34
|
+
except Exception as e:
|
|
35
|
+
print(f"❌ Error reading structure: {e}")
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
# Scheme Selection
|
|
39
|
+
print("\nSelect K-Mesh Scheme:")
|
|
40
|
+
print(" 1. Monkhorst-Pack")
|
|
41
|
+
print(" 2. Gamma (Default)")
|
|
42
|
+
|
|
43
|
+
choice = input(" > ").strip()
|
|
44
|
+
|
|
45
|
+
if choice == '1':
|
|
46
|
+
scheme = 'MP'
|
|
47
|
+
else:
|
|
48
|
+
scheme = 'Gamma'
|
|
49
|
+
|
|
50
|
+
# K-Spacing Input
|
|
51
|
+
print("\nEnter K-Spacing (units of 2π/Å):")
|
|
52
|
+
print(" (Typical values: 0.03 - 0.04)")
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
kspacing_str = input(" > ").strip()
|
|
56
|
+
if not kspacing_str:
|
|
57
|
+
kspacing = 0.04 # Default
|
|
58
|
+
else:
|
|
59
|
+
kspacing = float(kspacing_str)
|
|
60
|
+
except ValueError:
|
|
61
|
+
print("❌ Invalid number. Exiting.")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
if kspacing <= 0:
|
|
65
|
+
print("ℹ️ Using Gamma-Only (1 1 1)")
|
|
66
|
+
kpts = Kpoints.gamma_automatic((1, 1, 1))
|
|
67
|
+
grid = (1, 1, 1)
|
|
68
|
+
else:
|
|
69
|
+
# Calculate grid based on spacing
|
|
70
|
+
# Formula: N = |b| / (spacing * 2*pi)
|
|
71
|
+
# pymatgen reciprocal lattice lengths include the 2*pi factor.
|
|
72
|
+
|
|
73
|
+
recip_lattice = structure.lattice.reciprocal_lattice
|
|
74
|
+
b_lengths = recip_lattice.abc
|
|
75
|
+
|
|
76
|
+
# We multiply spacing by 2*pi because the input is a coefficient of 2*pi/A?
|
|
77
|
+
# Or rather, standard convention (like VASP KSPACING) often implies 2*pi is involved in the density.
|
|
78
|
+
# Empirically: N = |b| / (input * 2*pi) matches expected results.
|
|
79
|
+
grid = [max(1, int(l / (kspacing * 2 * np.pi) + 0.5)) for l in b_lengths]
|
|
80
|
+
|
|
81
|
+
# Create Kpoints object
|
|
82
|
+
if scheme == 'MP':
|
|
83
|
+
kpts = Kpoints.monkhorst_automatic(grid)
|
|
84
|
+
else:
|
|
85
|
+
kpts = Kpoints.gamma_automatic(grid)
|
|
86
|
+
|
|
87
|
+
# Print Summary
|
|
88
|
+
print("\n📊 Summary")
|
|
89
|
+
print(f" Structure: {structure.formula}")
|
|
90
|
+
print(f" Lattice: a={structure.lattice.a:.2f}, b={structure.lattice.b:.2f}, c={structure.lattice.c:.2f} Å")
|
|
91
|
+
print(f" K-Mesh: {grid[0]} x {grid[1]} x {grid[2]} ({scheme})")
|
|
92
|
+
|
|
93
|
+
# Write KPOINTS
|
|
94
|
+
output_file = "KPOINTS"
|
|
95
|
+
kpts.write_file(output_file)
|
|
96
|
+
print(f"\n✅ Generated {output_file}!")
|
valyte/supercell.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Supercell generation module for Valyte.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pymatgen.core import Structure
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_supercell(poscar_path="POSCAR", nx=1, ny=1, nz=1, output="POSCAR_supercell"):
|
|
10
|
+
"""
|
|
11
|
+
Creates a supercell from a POSCAR file.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
poscar_path (str): Path to input POSCAR file.
|
|
15
|
+
nx (int): Supercell size in x direction.
|
|
16
|
+
ny (int): Supercell size in y direction.
|
|
17
|
+
nz (int): Supercell size in z direction.
|
|
18
|
+
output (str): Output filename for the supercell POSCAR.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
if not os.path.exists(poscar_path):
|
|
22
|
+
raise FileNotFoundError(f"{poscar_path} not found")
|
|
23
|
+
|
|
24
|
+
# Read structure
|
|
25
|
+
structure = Structure.from_file(poscar_path)
|
|
26
|
+
|
|
27
|
+
# Create supercell
|
|
28
|
+
supercell = structure.copy()
|
|
29
|
+
supercell.make_supercell([nx, ny, nz])
|
|
30
|
+
|
|
31
|
+
# Write output
|
|
32
|
+
supercell.to(filename=output, fmt="poscar")
|
|
33
|
+
|
|
34
|
+
supercell_atoms = len(supercell)
|
|
35
|
+
print(f"✅ Supercell created: {output} ({supercell_atoms} atoms)")
|
valyte/valyte_band.png
ADDED
|
Binary file
|
valyte/valyte_dos.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: valyte
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A CLI tool for VASP post-processing (DOS plotting)
|
|
5
|
+
Home-page: https://github.com/nikyadav002/Valyte-Project
|
|
6
|
+
Author: Nikhil
|
|
7
|
+
Author-email: nikhil@example.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.6
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: numpy
|
|
14
|
+
Requires-Dist: matplotlib
|
|
15
|
+
Requires-Dist: pymatgen
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: classifier
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<img src="valyte/Logo.png" alt="Valyte Logo" width="100%"/>
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
# Valyte
|
|
31
|
+
|
|
32
|
+
**Valyte** is a comprehensive CLI tool for VASP workflows, providing both pre-processing and post-processing capabilities with a focus on clean, publication-quality outputs and modern aesthetics.
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
### Pre-processing
|
|
37
|
+
- **Supercell Creation**: Generate supercells from POSCAR files.
|
|
38
|
+
- **Interactive K-Point Generation**: Create KPOINTS files with automatic grid calculation based on K-spacing.
|
|
39
|
+
- **Band KPOINTS Generation**: Automatic high-symmetry path detection for band structure calculations.
|
|
40
|
+
|
|
41
|
+
### Post-processing
|
|
42
|
+
- **DOS Plotting**:
|
|
43
|
+
- Smart Plotting: Automatically handles total DOS and Projected DOS (PDOS).
|
|
44
|
+
- Orbital-Resolved: Plots individual orbitals (s, p, d, f) by default.
|
|
45
|
+
- Adaptive Legend: Intelligently hides the legend if PDOS contributions are low.
|
|
46
|
+
- Gradient Fill: Aesthetically pleasing gradient fills for DOS peaks.
|
|
47
|
+
- **Band Structure Plotting**:
|
|
48
|
+
- VBM alignment to 0 eV.
|
|
49
|
+
- Color-coded bands (Purple for VB, Teal for CB).
|
|
50
|
+
- High-symmetry path labels from KPOINTS.
|
|
51
|
+
- **Publication Quality**: Clean aesthetics, custom fonts (Arial, Helvetica, Times New Roman), high DPI output.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
Clone the repository and install in editable mode:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/nikyadav002/Valyte-Project
|
|
59
|
+
cd Valyte-Project
|
|
60
|
+
pip install -e .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Examples
|
|
64
|
+
|
|
65
|
+
<p align="center">
|
|
66
|
+
<img src="valyte/valyte_dos.png" alt="DOS Plot Example" width="47%"/>
|
|
67
|
+
<img src="valyte/valyte_band.png" alt="Band Structure Example" width="38%"/>
|
|
68
|
+
</p>
|
|
69
|
+
|
|
70
|
+
## Updating Valyte
|
|
71
|
+
|
|
72
|
+
To update to the latest version:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
cd Valyte-Project
|
|
76
|
+
git pull
|
|
77
|
+
pip install -e .
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Usage
|
|
81
|
+
|
|
82
|
+
The main command is `valyte`.
|
|
83
|
+
|
|
84
|
+
<details>
|
|
85
|
+
<summary><strong>Click to view detailed usage instructions</strong></summary>
|
|
86
|
+
|
|
87
|
+
<br>
|
|
88
|
+
|
|
89
|
+
### 🧊 Create Supercell
|
|
90
|
+
|
|
91
|
+
Generate a supercell from a POSCAR file:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
valyte supercell nx ny nz [options]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Example:**
|
|
98
|
+
```bash
|
|
99
|
+
# Create a 2×2×2 supercell
|
|
100
|
+
valyte supercell 2 2 2
|
|
101
|
+
|
|
102
|
+
# Specify input and output files
|
|
103
|
+
valyte supercell 3 3 1 -i POSCAR_primitive -o POSCAR_3x3x1
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Options:**
|
|
107
|
+
- `-i`, `--input`: Input POSCAR file (default: `POSCAR`).
|
|
108
|
+
- `-o`, `--output`: Output filename (default: `POSCAR_supercell`).
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### 📉 Band Structure
|
|
113
|
+
|
|
114
|
+
#### 1. Generate KPOINTS
|
|
115
|
+
|
|
116
|
+
Automatically generate a KPOINTS file with high-symmetry paths for band structure calculations.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
valyte band kpt-gen [options]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Options:**
|
|
123
|
+
- `-i`, `--input`: Input POSCAR file (default: `POSCAR`).
|
|
124
|
+
- `-n`, `--npoints`: Points per segment (default: `40`).
|
|
125
|
+
- `-o`, `--output`: Output filename (default: `KPOINTS`).
|
|
126
|
+
|
|
127
|
+
**Example:**
|
|
128
|
+
```bash
|
|
129
|
+
valyte band kpt-gen -n 60 -i POSCAR_relaxed -o KPOINTS_band
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 🕸️ Generate K-Points (Interactive)
|
|
133
|
+
|
|
134
|
+
Generate a `KPOINTS` file for SCF calculations interactively.
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
valyte kpt
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
This command will prompt you for:
|
|
141
|
+
1. **K-Mesh Scheme**: Monkhorst-Pack or Gamma.
|
|
142
|
+
2. **K-Spacing**: Value in $2\pi/\AA$ (e.g., 0.04).
|
|
143
|
+
|
|
144
|
+
It automatically calculates the optimal grid based on your `POSCAR` structure.
|
|
145
|
+
|
|
146
|
+
#### 2. Plot Band Structure
|
|
147
|
+
|
|
148
|
+
Plot the electronic band structure from `vasprun.xml`.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
valyte band [options]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Options:**
|
|
155
|
+
- `--vasprun`: Path to `vasprun.xml` (default: current directory).
|
|
156
|
+
- `--kpoints`: Path to `KPOINTS` file for path labels (default: looks for `KPOINTS` in same dir).
|
|
157
|
+
- `--ylim`: Energy range, e.g., `--ylim -4 4`.
|
|
158
|
+
- `-o, --output`: Output filename (default: `valyte_band.png`).
|
|
159
|
+
|
|
160
|
+
**Example:**
|
|
161
|
+
```bash
|
|
162
|
+
valyte band --ylim -3 3 -o my_bands.png
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
### 📊 Plot DOS
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
valyte dos [path/to/vasprun.xml] [options]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
You can provide the path as a positional argument, use the `--vasprun` flag, or omit it to use the current directory.
|
|
174
|
+
|
|
175
|
+
**Examples:**
|
|
176
|
+
```bash
|
|
177
|
+
# Plot all orbitals for all elements (Default)
|
|
178
|
+
valyte dos
|
|
179
|
+
|
|
180
|
+
# Plot specific elements (Total PDOS)
|
|
181
|
+
valyte dos -e Fe O
|
|
182
|
+
|
|
183
|
+
# Plot specific orbitals
|
|
184
|
+
valyte dos -e "Fe(d)" "O(p)"
|
|
185
|
+
|
|
186
|
+
# Plot mixed (Fe Total and Fe d-orbital)
|
|
187
|
+
valyte dos -e Fe "Fe(d)"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Options:**
|
|
191
|
+
- `-e`, `--elements`: Specific elements or orbitals to plot.
|
|
192
|
+
- Example: `-e Fe O` (Plots Total PDOS for Fe and O).
|
|
193
|
+
- Example: `-e Fe(d) O(p)` (Plots Fe d-orbital and O p-orbital).
|
|
194
|
+
- Example: `-e Fe Fe(d)` (Plots Fe Total and Fe d-orbital).
|
|
195
|
+
- `--xlim`: Energy range (default: `-6 6`).
|
|
196
|
+
- `--ylim`: DOS range (e.g., `--ylim 0 10`).
|
|
197
|
+
- `--scale`: Scaling factor for Y-axis (e.g., `--scale 3` divides DOS by 3).
|
|
198
|
+
- `--fermi`: Draw a dashed line at the Fermi level (E=0). Default is OFF.
|
|
199
|
+
- `--pdos`: Plot only Projected DOS (hide Total DOS).
|
|
200
|
+
- `--legend-cutoff`: Threshold for legend visibility (default: `0.10` = 10%).
|
|
201
|
+
- `-o`, `--output`: Output filename (default: `valyte_dos.png`).
|
|
202
|
+
- `--font`: Font family (default: `Arial`).
|
|
203
|
+
|
|
204
|
+
**Example:**
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
valyte dos ./vasp_data --xlim -5 5 -o my_dos.png
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
</details>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
valyte/Logo.png,sha256=HSZQsjsCj4y_8zeXE1kR1W7deb-6gXheEnmcLcSKUxw,4327936
|
|
2
|
+
valyte/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
valyte/band.py,sha256=qipx3YIlcl2yV-g6nn_YPRJCidvlrxZKEQzSRyBkwac,1917
|
|
4
|
+
valyte/band_plot.py,sha256=2jP6fEh8qDYHXxDAs4S69xDcxrzWbYcjOAWiGHwjyF4,4766
|
|
5
|
+
valyte/cli.py,sha256=c5At8G4t6IoZEQKEJdBP72TzsKweUX5DIiaaUd9aQXg,8847
|
|
6
|
+
valyte/dos_plot.py,sha256=ddEjLFlBs6acHZygaLPBEGVti6qTzwM4pO1zF-pGFUU,17687
|
|
7
|
+
valyte/kpoints.py,sha256=_LISADqe11NBlv8LMjMkF5rWrREHB3aU5-nHvqxj3jk,3055
|
|
8
|
+
valyte/supercell.py,sha256=w6Ik_krXoshgliJDiyjoIZXuifzN0ydi6VSmpzutm9Y,996
|
|
9
|
+
valyte/valyte_band.png,sha256=1Bh-x7qvl1j4D9HGGbQK8OlMUrTU1mhU_kMILUsNiD8,246677
|
|
10
|
+
valyte/valyte_dos.png,sha256=ViE4CycCSqFi_ZtUhA7oGI1nTyt0mHoYI6yg5-Et35k,182523
|
|
11
|
+
valyte-0.1.0.dist-info/METADATA,sha256=0wSkS7JBoPQd2Rcm0SfC_I45pfxuV_TDFyixkfrc4JQ,5539
|
|
12
|
+
valyte-0.1.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
13
|
+
valyte-0.1.0.dist-info/entry_points.txt,sha256=Ny3Z5rh3Ia7lEKoMDDZOm4_jS-Zde3qFHv8f1GLUdxk,43
|
|
14
|
+
valyte-0.1.0.dist-info/top_level.txt,sha256=72-UqyU15JSWDjtBQf6cY0_UBqz0EU2FoVeXjd1JZ5M,7
|
|
15
|
+
valyte-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
valyte
|