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 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
- def generate_band_kpoints(poscar_path="POSCAR", npoints=40, output="KPOINTS"):
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
- # 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
-
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
- 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)")
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}")
File without changes