valyte 0.1.7__py3-none-any.whl → 0.1.9__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/band.py +263 -30
- valyte/cli.py +24 -1
- valyte/data/__init__.py +0 -0
- valyte/data/bradcrack.json +1194 -0
- valyte/kpoints.py +12 -0
- valyte/potcar.py +61 -0
- {valyte-0.1.7.dist-info → valyte-0.1.9.dist-info}/METADATA +17 -12
- valyte-0.1.9.dist-info/RECORD +18 -0
- {valyte-0.1.7.dist-info → valyte-0.1.9.dist-info}/WHEEL +1 -1
- valyte-0.1.7.dist-info/RECORD +0 -15
- {valyte-0.1.7.dist-info → valyte-0.1.9.dist-info}/entry_points.txt +0 -0
- {valyte-0.1.7.dist-info → valyte-0.1.9.dist-info}/top_level.txt +0 -0
valyte/band.py
CHANGED
|
@@ -3,19 +3,35 @@ Band structure KPOINTS generation module for Valyte.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import json
|
|
7
|
+
import numpy as np
|
|
8
|
+
import seekpath
|
|
9
|
+
import spglib
|
|
6
10
|
from pymatgen.core import Structure
|
|
7
11
|
from pymatgen.symmetry.bandstructure import HighSymmKpath
|
|
12
|
+
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
|
|
13
|
+
try:
|
|
14
|
+
from importlib.resources import files as ilr_files
|
|
15
|
+
except ImportError:
|
|
16
|
+
import importlib_resources as ilr_files
|
|
8
17
|
|
|
18
|
+
from valyte.potcar import generate_potcar
|
|
9
19
|
|
|
10
|
-
|
|
20
|
+
|
|
21
|
+
def generate_band_kpoints(poscar_path="POSCAR", npoints=40, output="KPOINTS", symprec=0.01, mode="bradcrack"):
|
|
11
22
|
"""
|
|
12
23
|
Generates KPOINTS file in line-mode for band structure calculations.
|
|
13
24
|
Uses SeeK-path method for high-symmetry path determination.
|
|
14
25
|
|
|
26
|
+
IMPORTANT: Writes a standardized POSCAR (POSCAR_standard) that MUST be used
|
|
27
|
+
for the band structure calculation to ensure K-points are valid.
|
|
28
|
+
|
|
15
29
|
Args:
|
|
16
30
|
poscar_path (str): Path to input POSCAR file.
|
|
17
31
|
npoints (int): Number of points per segment (default: 40).
|
|
18
32
|
output (str): Output filename for KPOINTS.
|
|
33
|
+
symprec (float): Symmetry precision for standardization (default: 0.01).
|
|
34
|
+
mode (str): Standardization convention (default: "bradcrack").
|
|
19
35
|
"""
|
|
20
36
|
|
|
21
37
|
if not os.path.exists(poscar_path):
|
|
@@ -24,33 +40,250 @@ def generate_band_kpoints(poscar_path="POSCAR", npoints=40, output="KPOINTS"):
|
|
|
24
40
|
# Read structure
|
|
25
41
|
structure = Structure.from_file(poscar_path)
|
|
26
42
|
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
# --- K-Point Generation Logic ---
|
|
44
|
+
if mode == "bradcrack":
|
|
45
|
+
try:
|
|
46
|
+
kpath = BradCrackKpath(structure, symprec=symprec)
|
|
47
|
+
prim_std = kpath.prim
|
|
48
|
+
path = kpath.path
|
|
49
|
+
kpoints = kpath.kpoints
|
|
50
|
+
|
|
51
|
+
# Write standardized POSCAR from BradCrack logic
|
|
52
|
+
standard_filename = "POSCAR_standard"
|
|
53
|
+
prim_std.to(filename=standard_filename)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
print(f"❌ Error generating BradCrack path: {e}")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
else:
|
|
59
|
+
# Fallback to Pymatgen logic for other modes
|
|
60
|
+
try:
|
|
61
|
+
# Map 'seekpath' alias to 'hinuma' which pymatgen uses (wrapper around seekpath)
|
|
62
|
+
if mode == "seekpath":
|
|
63
|
+
mode = "hinuma"
|
|
64
|
+
|
|
65
|
+
# Standardize structure first using SpacegroupAnalyzer
|
|
66
|
+
sga = SpacegroupAnalyzer(structure, symprec=symprec)
|
|
67
|
+
prim_std = sga.get_primitive_standard_structure()
|
|
68
|
+
except Exception as e:
|
|
69
|
+
print(f"❌ Error during standardization: {e}")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
# Get high-symmetry path for the STANDARDIZED structure
|
|
73
|
+
try:
|
|
74
|
+
kpath = HighSymmKpath(prim_std, path_type=mode, symprec=symprec)
|
|
75
|
+
|
|
76
|
+
# Write the standardized primitive structure
|
|
77
|
+
standard_filename = "POSCAR_standard"
|
|
78
|
+
prim_std.to(filename=standard_filename)
|
|
79
|
+
|
|
80
|
+
# Get the path
|
|
81
|
+
path = kpath.kpath["path"]
|
|
82
|
+
kpoints = kpath.kpath["kpoints"]
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print(f"❌ Error generating K-path: {e}")
|
|
85
|
+
return
|
|
86
|
+
|
|
34
87
|
# Write KPOINTS file
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
88
|
+
try:
|
|
89
|
+
with open(output, "w") as f:
|
|
90
|
+
f.write("KPOINTS for Band Structure\n")
|
|
91
|
+
f.write(f"{npoints}\n")
|
|
92
|
+
f.write("Line-mode\n")
|
|
93
|
+
f.write("Reciprocal\n")
|
|
94
|
+
|
|
95
|
+
for subpath in path:
|
|
96
|
+
for i in range(len(subpath) - 1):
|
|
97
|
+
start_label = subpath[i]
|
|
98
|
+
end_label = subpath[i+1]
|
|
99
|
+
|
|
100
|
+
start_coords = kpoints[start_label]
|
|
101
|
+
end_coords = kpoints[end_label]
|
|
102
|
+
|
|
103
|
+
f.write(f"{start_coords[0]:10.6f} {start_coords[1]:10.6f} {start_coords[2]:10.6f} ! {start_label}\n")
|
|
104
|
+
f.write(f"{end_coords[0]:10.6f} {end_coords[1]:10.6f} {end_coords[2]:10.6f} ! {end_label}\n")
|
|
105
|
+
f.write("\n") # Optional newline between segments
|
|
106
|
+
|
|
107
|
+
print(f"✅ Generated {output} ({' - '.join([' - '.join(seg) for seg in path])})")
|
|
108
|
+
print(f"✅ Generated {standard_filename} (Standardized Primitive Cell)")
|
|
109
|
+
print(f"\n⚠️ IMPORTANT: You MUST use '{standard_filename}' for your band calculation!")
|
|
110
|
+
print(f" The K-points are generated for this standardized orientation.")
|
|
111
|
+
print(f" Using your original POSCAR may result in incorrect paths or 'Reciprocal lattice' errors.")
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
print(f"❌ Error writing KPOINTS file: {e}")
|
|
115
|
+
|
|
116
|
+
# --- POTCAR Generation ---
|
|
117
|
+
try:
|
|
118
|
+
print("ℹ️ Generating default POTCAR (PBE)...")
|
|
119
|
+
generate_potcar(poscar_path=poscar_path, functional="PBE", output="POTCAR")
|
|
120
|
+
except Exception as e:
|
|
121
|
+
print(f"⚠️ Could not generate POTCAR: {e}")
|
|
122
|
+
print(" (Proceeding without stopping, as KPOINTS are already generated)")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class BradCrackKpath:
|
|
127
|
+
"""
|
|
128
|
+
Native implementation of Bradley-Cracknell K-path generation.
|
|
129
|
+
Replicates logic from Sumo/SeeK-path to determine standard paths.
|
|
130
|
+
"""
|
|
131
|
+
def __init__(self, structure, symprec=0.01):
|
|
132
|
+
self.structure = structure
|
|
133
|
+
self.symprec = symprec
|
|
134
|
+
|
|
135
|
+
# Use SpacegroupAnalyzer for basic data
|
|
136
|
+
sga = SpacegroupAnalyzer(structure, symprec=symprec)
|
|
137
|
+
self._spg_data = sga.get_symmetry_dataset()
|
|
138
|
+
|
|
139
|
+
# Use SeeK-path to get primitive/conventional structures matches Sumo Kpath.__init__
|
|
140
|
+
|
|
141
|
+
# refine_cell logic from Sumo base class
|
|
142
|
+
# atom_numbers = [site.specie.number for site in structure]
|
|
143
|
+
# But pymatgen structure to spglib cell tuple:
|
|
144
|
+
# cell = (lattice, positions, numbers)
|
|
145
|
+
cell = (structure.lattice.matrix, structure.frac_coords, [s.specie.number for s in structure])
|
|
146
|
+
|
|
147
|
+
# Sumo uses spglib.refine_cell on the cell first?
|
|
148
|
+
# "std = spglib.refine_cell(sym._cell, symprec=symprec)"
|
|
149
|
+
# pymatgen sga._cell is (lattice, positions, numbers)
|
|
150
|
+
|
|
151
|
+
# seekpath.get_path takes the cell structure
|
|
152
|
+
# output is dictionary
|
|
153
|
+
self._seek_data = seekpath.get_path(cell, symprec=symprec)
|
|
154
|
+
|
|
155
|
+
# Reconstruct primitive structure from seekpath output
|
|
156
|
+
prim_lattice = self._seek_data["primitive_lattice"]
|
|
157
|
+
prim_pos = self._seek_data["primitive_positions"]
|
|
158
|
+
prim_types = self._seek_data["primitive_types"]
|
|
159
|
+
# Map types back to species?
|
|
160
|
+
# We need a map from number to Element.
|
|
161
|
+
# unique_species from sga?
|
|
162
|
+
# Let's just use explicit element list from input structure, assuming types are consistent?
|
|
163
|
+
# Or better, use sga to map Z to elements.
|
|
164
|
+
|
|
165
|
+
# Setup element mapping
|
|
166
|
+
# Create a map from atomic number to Element object from input structure
|
|
167
|
+
z_to_specie = {s.specie.number: s.specie for s in structure}
|
|
168
|
+
prim_species = [z_to_specie[z] for z in prim_types]
|
|
169
|
+
|
|
170
|
+
self.prim = Structure(prim_lattice, prim_species, prim_pos)
|
|
171
|
+
|
|
172
|
+
conv_lattice = self._seek_data["conv_lattice"]
|
|
173
|
+
conv_pos = self._seek_data["conv_positions"]
|
|
174
|
+
conv_types = self._seek_data["conv_types"]
|
|
175
|
+
conv_species = [z_to_specie[z] for z in conv_types]
|
|
176
|
+
self.conv = Structure(conv_lattice, conv_species, conv_pos)
|
|
177
|
+
|
|
178
|
+
# Now determine Bravais lattice for BradCrack
|
|
179
|
+
self._get_bradcrack_path()
|
|
180
|
+
|
|
181
|
+
def _get_bradcrack_path(self):
|
|
182
|
+
|
|
183
|
+
# Determine lattice parameters from CONVENTIONAL cell
|
|
184
|
+
a, b, c = self.conv.lattice.abc
|
|
185
|
+
angles = self.conv.lattice.angles
|
|
186
|
+
# finding unique axis for monoclinic
|
|
187
|
+
# logic from BradCrackKpath.__init__
|
|
188
|
+
# "unique = angles.index(min(angles, key=angles.count))"
|
|
189
|
+
# usually 90, 90, beta. So unique is beta (non-90) index? No.
|
|
190
|
+
# Monoclinic: alpha=gamma=90, beta!=90. 90 appears twice. non-90 appears once.
|
|
191
|
+
# min count of angle values?
|
|
192
|
+
# if angles are [90, 90, 105], counts are {90:2, 105:1}. min count is 1. value is 105. index is 2.
|
|
193
|
+
# so unique is index of non-90 degree angle.
|
|
194
|
+
|
|
195
|
+
# Round angles to avoid float issues
|
|
196
|
+
angles_r = [round(x, 3) for x in angles]
|
|
197
|
+
unique_val = min(angles_r, key=angles_r.count)
|
|
198
|
+
unique = angles_r.index(unique_val)
|
|
199
|
+
|
|
200
|
+
# Get Space Group Symbol and Number
|
|
201
|
+
# From seekpath? or sga?
|
|
202
|
+
# Sumo uses: "spg_symbol = self.spg_symbol" which is "self._spg_data['international']"
|
|
203
|
+
# spglib dataset returns 'international'
|
|
204
|
+
spg_symbol = self._spg_data["international"]
|
|
205
|
+
spg_number = self._spg_data["number"]
|
|
206
|
+
|
|
207
|
+
lattice_type = self.get_lattice_type(spg_number)
|
|
208
|
+
|
|
209
|
+
bravais = self._get_bravais_lattice(spg_symbol, lattice_type, a, b, c, unique)
|
|
210
|
+
|
|
211
|
+
# Load JSON
|
|
212
|
+
|
|
213
|
+
json_file = ilr_files("valyte.data").joinpath("bradcrack.json")
|
|
214
|
+
with open(json_file, 'r') as f:
|
|
215
|
+
data = json.load(f)
|
|
216
|
+
|
|
217
|
+
if bravais not in data:
|
|
218
|
+
raise ValueError(f"Bravais lattice code '{bravais}' not found in BradCrack data.")
|
|
219
|
+
|
|
220
|
+
self.bradcrack_data = data[bravais]
|
|
221
|
+
self.kpoints = self.bradcrack_data["kpoints"]
|
|
222
|
+
self.path = self.bradcrack_data["path"]
|
|
223
|
+
|
|
224
|
+
def get_lattice_type(self, number):
|
|
225
|
+
# Logic from Sumo
|
|
226
|
+
if 1 <= number <= 2: return "triclinic"
|
|
227
|
+
if 3 <= number <= 15: return "monoclinic"
|
|
228
|
+
if 16 <= number <= 74: return "orthorhombic"
|
|
229
|
+
if 75 <= number <= 142: return "tetragonal"
|
|
230
|
+
if 143 <= number <= 167:
|
|
231
|
+
if number in [146, 148, 155, 160, 161, 166, 167]: return "rhombohedral"
|
|
232
|
+
return "trigonal"
|
|
233
|
+
if 168 <= number <= 194: return "hexagonal"
|
|
234
|
+
if 195 <= number <= 230: return "cubic"
|
|
235
|
+
return "unknown"
|
|
236
|
+
|
|
237
|
+
def _get_bravais_lattice(self, spg_symbol, lattice_type, a, b, c, unique):
|
|
238
|
+
# Logic from Sumo BradCrackKpath._get_bravais_lattice
|
|
239
|
+
if lattice_type == "triclinic": return "triclinic"
|
|
240
|
+
|
|
241
|
+
elif lattice_type == "monoclinic":
|
|
242
|
+
if "P" in spg_symbol:
|
|
243
|
+
if unique == 0: return "mon_p_a"
|
|
244
|
+
elif unique == 1: return "mon_p_b"
|
|
245
|
+
elif unique == 2: return "mon_p_c"
|
|
246
|
+
elif "C" in spg_symbol:
|
|
247
|
+
if unique == 0: return "mon_c_a"
|
|
248
|
+
elif unique == 1: return "mon_c_b"
|
|
249
|
+
elif unique == 2: return "mon_c_c"
|
|
250
|
+
|
|
251
|
+
elif lattice_type == "orthorhombic":
|
|
252
|
+
if "P" in spg_symbol: return "orth_p"
|
|
253
|
+
elif "A" in spg_symbol or "C" in spg_symbol:
|
|
254
|
+
if a > b: return "orth_c_a"
|
|
255
|
+
elif b > a: return "orth_c_b"
|
|
256
|
+
elif "F" in spg_symbol:
|
|
257
|
+
# 1/a^2 etc conditions... need to replicate exact math
|
|
258
|
+
# Copied from Sumo source view
|
|
259
|
+
inv_a2 = 1/a**2; inv_b2 = 1/b**2; inv_c2 = 1/c**2
|
|
260
|
+
if (inv_a2 < inv_b2 + inv_c2) and (inv_b2 < inv_c2 + inv_a2) and (inv_c2 < inv_a2 + inv_b2):
|
|
261
|
+
return "orth_f_1"
|
|
262
|
+
elif inv_c2 > inv_a2 + inv_b2: return "orth_f_2"
|
|
263
|
+
elif inv_b2 > inv_a2 + inv_c2: return "orth_f_3"
|
|
264
|
+
elif inv_a2 > inv_c2 + inv_b2: return "orth_f_4"
|
|
265
|
+
elif "I" in spg_symbol:
|
|
266
|
+
if a > b and a > c: return "orth_i_a"
|
|
267
|
+
elif b > a and b > c: return "orth_i_b"
|
|
268
|
+
elif c > a and c > b: return "orth_i_c"
|
|
269
|
+
|
|
270
|
+
elif lattice_type == "tetragonal":
|
|
271
|
+
if "P" in spg_symbol: return "tet_p"
|
|
272
|
+
elif "I" in spg_symbol:
|
|
273
|
+
if a > c: return "tet_i_a"
|
|
274
|
+
else: return "tet_i_c"
|
|
275
|
+
|
|
276
|
+
elif lattice_type in ["trigonal", "hexagonal", "rhombohedral"]:
|
|
277
|
+
if "R" in spg_symbol:
|
|
278
|
+
if a > np.sqrt(2)*c: return "trig_r_a"
|
|
279
|
+
else: return "trig_r_c"
|
|
280
|
+
elif "P" in spg_symbol:
|
|
281
|
+
if unique == 0: return "trig_p_a"
|
|
282
|
+
elif unique == 2: return "trig_p_c"
|
|
283
|
+
|
|
284
|
+
elif lattice_type == "cubic":
|
|
285
|
+
if "P" in spg_symbol: return "cubic_p"
|
|
286
|
+
elif "I" in spg_symbol: return "cubic_i"
|
|
287
|
+
elif "F" in spg_symbol: return "cubic_f"
|
|
288
|
+
|
|
289
|
+
return "unknown"
|
valyte/cli.py
CHANGED
|
@@ -27,6 +27,7 @@ from valyte.band import generate_band_kpoints
|
|
|
27
27
|
from valyte.band_plot import plot_band_structure
|
|
28
28
|
from valyte.dos_plot import load_dos, plot_dos
|
|
29
29
|
from valyte.kpoints import generate_kpoints_interactive
|
|
30
|
+
from valyte.potcar import generate_potcar
|
|
30
31
|
|
|
31
32
|
def parse_element_selection(inputs):
|
|
32
33
|
"""
|
|
@@ -116,10 +117,19 @@ def main():
|
|
|
116
117
|
kpt_gen_parser.add_argument("-i", "--input", default="POSCAR", help="Input POSCAR file")
|
|
117
118
|
kpt_gen_parser.add_argument("-n", "--npoints", type=int, default=40, help="Points per segment")
|
|
118
119
|
kpt_gen_parser.add_argument("-o", "--output", default="KPOINTS", help="Output filename")
|
|
120
|
+
kpt_gen_parser.add_argument("--symprec", type=float, default=0.01, help="Symmetry precision (default: 0.01)")
|
|
121
|
+
|
|
122
|
+
kpt_gen_parser.add_argument("--mode", default="bradcrack", help="Standardization mode (default: bradcrack)")
|
|
119
123
|
|
|
120
124
|
# --- K-Point Generation (Interactive) ---
|
|
121
125
|
subparsers.add_parser("kpt", help="Interactive K-Point Generation (SCF)")
|
|
122
126
|
|
|
127
|
+
# --- POTCAR Generation ---
|
|
128
|
+
potcar_parser = subparsers.add_parser("potcar", help="Generate POTCAR")
|
|
129
|
+
potcar_parser.add_argument("-i", "--input", default="POSCAR", help="Input POSCAR file")
|
|
130
|
+
potcar_parser.add_argument("-o", "--output", default="POTCAR", help="Output filename")
|
|
131
|
+
potcar_parser.add_argument("--functional", default="PBE", help="Functional (default: PBE)")
|
|
132
|
+
|
|
123
133
|
args = parser.parse_args()
|
|
124
134
|
|
|
125
135
|
if args.command == "dos":
|
|
@@ -169,13 +179,26 @@ def main():
|
|
|
169
179
|
print(f"❌ Error: {e}")
|
|
170
180
|
sys.exit(1)
|
|
171
181
|
|
|
182
|
+
elif args.command == "potcar":
|
|
183
|
+
try:
|
|
184
|
+
generate_potcar(
|
|
185
|
+
poscar_path=args.input,
|
|
186
|
+
functional=args.functional,
|
|
187
|
+
output=args.output
|
|
188
|
+
)
|
|
189
|
+
except Exception as e:
|
|
190
|
+
print(f"❌ Error: {e}")
|
|
191
|
+
sys.exit(1)
|
|
192
|
+
|
|
172
193
|
elif args.command == "band":
|
|
173
194
|
if args.band_command == "kpt-gen":
|
|
174
195
|
try:
|
|
175
196
|
generate_band_kpoints(
|
|
176
197
|
poscar_path=args.input,
|
|
177
198
|
npoints=args.npoints,
|
|
178
|
-
output=args.output
|
|
199
|
+
output=args.output,
|
|
200
|
+
symprec=args.symprec,
|
|
201
|
+
mode=args.mode
|
|
179
202
|
)
|
|
180
203
|
except Exception as e:
|
|
181
204
|
print(f"❌ Error: {e}")
|
valyte/data/__init__.py
ADDED
|
File without changes
|