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