valyte 0.1.8__py3-none-any.whl → 0.1.11__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
@@ -1,278 +1,247 @@
1
- """
2
- Band structure KPOINTS generation module for Valyte.
3
- """
1
+ """Band structure KPOINTS generation."""
4
2
 
5
3
  import os
6
4
  import json
7
5
  import numpy as np
8
6
  import seekpath
9
- import spglib
10
7
  from pymatgen.core import Structure
11
8
  from pymatgen.symmetry.bandstructure import HighSymmKpath
12
9
  from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
13
10
  try:
14
11
  from importlib.resources import files as ilr_files
15
12
  except ImportError:
16
- import importlib_resources as ilr_files
13
+ from importlib_resources import files as ilr_files
14
+
15
+ from valyte.potcar import generate_potcar
17
16
 
18
17
 
19
18
  def generate_band_kpoints(poscar_path="POSCAR", npoints=40, output="KPOINTS", symprec=0.01, mode="bradcrack"):
20
- """
21
- Generates KPOINTS file in line-mode for band structure calculations.
22
- Uses SeeK-path method for high-symmetry path determination.
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
-
27
- Args:
28
- poscar_path (str): Path to input POSCAR file.
29
- npoints (int): Number of points per segment (default: 40).
30
- output (str): Output filename for KPOINTS.
31
- symprec (float): Symmetry precision for standardization (default: 0.01).
32
- mode (str): Standardization convention (default: "bradcrack").
33
- """
34
-
19
+ """Generate a line-mode KPOINTS file for band structure calculations."""
20
+
35
21
  if not os.path.exists(poscar_path):
36
22
  raise FileNotFoundError(f"{poscar_path} not found")
37
-
38
- # Read structure
23
+
24
+ mode = (mode or "bradcrack").lower()
25
+
39
26
  structure = Structure.from_file(poscar_path)
40
-
41
- # --- K-Point Generation Logic ---
27
+
42
28
  if mode == "bradcrack":
43
29
  try:
44
30
  kpath = BradCrackKpath(structure, symprec=symprec)
45
31
  prim_std = kpath.prim
46
32
  path = kpath.path
47
33
  kpoints = kpath.kpoints
48
-
49
- # Write standardized POSCAR from BradCrack logic
34
+
50
35
  standard_filename = "POSCAR_standard"
51
36
  prim_std.to(filename=standard_filename)
52
37
  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
38
+ raise RuntimeError(f"Error generating Bradley-Cracknell path: {e}")
39
+ else:
58
40
  try:
59
- # Map 'seekpath' alias to 'hinuma' which pymatgen uses (wrapper around seekpath)
60
41
  if mode == "seekpath":
61
42
  mode = "hinuma"
62
43
 
63
- # Standardize structure first using SpacegroupAnalyzer
64
44
  sga = SpacegroupAnalyzer(structure, symprec=symprec)
65
45
  prim_std = sga.get_primitive_standard_structure()
66
46
  except Exception as e:
67
- print(f"Error during standardization: {e}")
68
- return
47
+ raise RuntimeError(f"Error during standardization: {e}")
69
48
 
70
- # Get high-symmetry path for the STANDARDIZED structure
71
49
  try:
72
50
  kpath = HighSymmKpath(prim_std, path_type=mode, symprec=symprec)
73
-
74
- # Write the standardized primitive structure
51
+
75
52
  standard_filename = "POSCAR_standard"
76
53
  prim_std.to(filename=standard_filename)
77
-
78
- # Get the path
54
+
79
55
  path = kpath.kpath["path"]
80
56
  kpoints = kpath.kpath["kpoints"]
81
57
  except Exception as e:
82
- print(f"Error generating K-path: {e}")
83
- return
58
+ raise RuntimeError(f"Error generating K-path: {e}")
84
59
 
85
- # Write KPOINTS file
86
60
  try:
87
61
  with open(output, "w") as f:
88
62
  f.write("KPOINTS for Band Structure\n")
89
63
  f.write(f"{npoints}\n")
90
64
  f.write("Line-mode\n")
91
65
  f.write("Reciprocal\n")
92
-
66
+
93
67
  for subpath in path:
94
68
  for i in range(len(subpath) - 1):
95
69
  start_label = subpath[i]
96
- end_label = subpath[i+1]
97
-
70
+ end_label = subpath[i + 1]
71
+
98
72
  start_coords = kpoints[start_label]
99
73
  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
74
 
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.")
75
+ f.write(
76
+ f"{start_coords[0]:10.6f} {start_coords[1]:10.6f} {start_coords[2]:10.6f} ! {start_label}\n"
77
+ )
78
+ f.write(
79
+ f"{end_coords[0]:10.6f} {end_coords[1]:10.6f} {end_coords[2]:10.6f} ! {end_label}\n"
80
+ )
81
+ f.write("\n")
82
+
83
+ print(f"Generated {output} ({' - '.join([' - '.join(seg) for seg in path])})")
84
+ print(f"Generated {standard_filename} (Standardized Primitive Cell)")
85
+ print("IMPORTANT: Use POSCAR_standard for the band calculation.")
86
+ except Exception as e:
87
+ raise RuntimeError(f"Error writing KPOINTS file: {e}")
110
88
 
89
+ try:
90
+ print("Generating default POTCAR (PBE)...")
91
+ generate_potcar(poscar_path=poscar_path, functional="PBE", output="POTCAR")
111
92
  except Exception as e:
112
- print(f" Error writing KPOINTS file: {e}")
93
+ print(f"Warning: could not generate POTCAR: {e}")
94
+ print("Proceeding without POTCAR generation.")
113
95
 
114
96
 
115
97
  class BradCrackKpath:
116
- """
117
- Native implementation of Bradley-Cracknell K-path generation.
118
- Replicates logic from Sumo/SeeK-path to determine standard paths.
119
- """
98
+ """Bradley-Cracknell K-path generation using SeeK-path output."""
99
+
120
100
  def __init__(self, structure, symprec=0.01):
121
101
  self.structure = structure
122
102
  self.symprec = symprec
123
-
124
- # Use SpacegroupAnalyzer for basic data
103
+
125
104
  sga = SpacegroupAnalyzer(structure, symprec=symprec)
126
105
  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
106
+
107
+ cell = (
108
+ structure.lattice.matrix,
109
+ structure.frac_coords,
110
+ [s.specie.number for s in structure],
111
+ )
112
+
142
113
  self._seek_data = seekpath.get_path(cell, symprec=symprec)
143
-
144
- # Reconstruct primitive structure from seekpath output
114
+
145
115
  prim_lattice = self._seek_data["primitive_lattice"]
146
116
  prim_pos = self._seek_data["primitive_positions"]
147
117
  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
118
+
156
119
  z_to_specie = {s.specie.number: s.specie for s in structure}
157
120
  prim_species = [z_to_specie[z] for z in prim_types]
158
-
121
+
159
122
  self.prim = Structure(prim_lattice, prim_species, prim_pos)
160
-
123
+
161
124
  conv_lattice = self._seek_data["conv_lattice"]
162
125
  conv_pos = self._seek_data["conv_positions"]
163
126
  conv_types = self._seek_data["conv_types"]
164
127
  conv_species = [z_to_specie[z] for z in conv_types]
165
128
  self.conv = Structure(conv_lattice, conv_species, conv_pos)
166
-
167
- # Now determine Bravais lattice for BradCrack
129
+
168
130
  self._get_bradcrack_path()
169
131
 
170
132
  def _get_bradcrack_path(self):
171
-
172
- # Determine lattice parameters from CONVENTIONAL cell
173
133
  a, b, c = self.conv.lattice.abc
174
134
  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
135
+
185
136
  angles_r = [round(x, 3) for x in angles]
186
137
  unique_val = min(angles_r, key=angles_r.count)
187
138
  unique = angles_r.index(unique_val)
188
139
 
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
140
  spg_symbol = self._spg_data["international"]
194
141
  spg_number = self._spg_data["number"]
195
-
142
+
196
143
  lattice_type = self.get_lattice_type(spg_number)
197
-
198
144
  bravais = self._get_bravais_lattice(spg_symbol, lattice_type, a, b, c, unique)
199
-
200
- # Load JSON
201
-
145
+
202
146
  json_file = ilr_files("valyte.data").joinpath("bradcrack.json")
203
- with open(json_file, 'r') as f:
147
+ with json_file.open("r") as f:
204
148
  data = json.load(f)
205
-
149
+
206
150
  if bravais not in data:
207
151
  raise ValueError(f"Bravais lattice code '{bravais}' not found in BradCrack data.")
208
-
152
+
209
153
  self.bradcrack_data = data[bravais]
210
154
  self.kpoints = self.bradcrack_data["kpoints"]
211
155
  self.path = self.bradcrack_data["path"]
212
156
 
213
157
  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"
158
+ if 1 <= number <= 2:
159
+ return "triclinic"
160
+ if 3 <= number <= 15:
161
+ return "monoclinic"
162
+ if 16 <= number <= 74:
163
+ return "orthorhombic"
164
+ if 75 <= number <= 142:
165
+ return "tetragonal"
166
+ if 143 <= number <= 167:
167
+ if number in [146, 148, 155, 160, 161, 166, 167]:
168
+ return "rhombohedral"
221
169
  return "trigonal"
222
- if 168 <= number <= 194: return "hexagonal"
223
- if 195 <= number <= 230: return "cubic"
170
+ if 168 <= number <= 194:
171
+ return "hexagonal"
172
+ if 195 <= number <= 230:
173
+ return "cubic"
224
174
  return "unknown"
225
175
 
226
176
  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":
177
+ if lattice_type == "triclinic":
178
+ return "triclinic"
179
+
180
+ if lattice_type == "monoclinic":
181
+ if "P" in spg_symbol:
182
+ if unique == 0:
183
+ return "mon_p_a"
184
+ if unique == 1:
185
+ return "mon_p_b"
186
+ if unique == 2:
187
+ return "mon_p_c"
188
+ if "C" in spg_symbol:
189
+ if unique == 0:
190
+ return "mon_c_a"
191
+ if unique == 1:
192
+ return "mon_c_b"
193
+ if unique == 2:
194
+ return "mon_c_c"
195
+
196
+ if lattice_type == "orthorhombic":
231
197
  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
198
+ return "orth_p"
199
+ if "A" in spg_symbol or "C" in spg_symbol:
200
+ if a > b:
201
+ return "orth_c_a"
202
+ if b > a:
203
+ return "orth_c_b"
204
+ if "F" in spg_symbol:
205
+ inv_a2 = 1 / a**2
206
+ inv_b2 = 1 / b**2
207
+ inv_c2 = 1 / c**2
249
208
  if (inv_a2 < inv_b2 + inv_c2) and (inv_b2 < inv_c2 + inv_a2) and (inv_c2 < inv_a2 + inv_b2):
250
209
  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"]:
210
+ if inv_c2 > inv_a2 + inv_b2:
211
+ return "orth_f_2"
212
+ if inv_b2 > inv_a2 + inv_c2:
213
+ return "orth_f_3"
214
+ if inv_a2 > inv_c2 + inv_b2:
215
+ return "orth_f_4"
216
+ if "I" in spg_symbol:
217
+ if a > b and a > c:
218
+ return "orth_i_a"
219
+ if b > a and b > c:
220
+ return "orth_i_b"
221
+ if c > a and c > b:
222
+ return "orth_i_c"
223
+
224
+ if lattice_type == "tetragonal":
225
+ if "P" in spg_symbol:
226
+ return "tet_p"
227
+ if "I" in spg_symbol:
228
+ return "tet_i_a" if a > c else "tet_i_c"
229
+
230
+ if lattice_type in ["trigonal", "hexagonal", "rhombohedral"]:
266
231
  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
-
232
+ return "trig_r_a" if a > np.sqrt(2) * c else "trig_r_c"
233
+ if "P" in spg_symbol:
234
+ if unique == 0:
235
+ return "trig_p_a"
236
+ if unique == 2:
237
+ return "trig_p_c"
238
+
239
+ if lattice_type == "cubic":
240
+ if "P" in spg_symbol:
241
+ return "cubic_p"
242
+ if "I" in spg_symbol:
243
+ return "cubic_i"
244
+ if "F" in spg_symbol:
245
+ return "cubic_f"
246
+
278
247
  return "unknown"
valyte/band_plot.py CHANGED
@@ -4,25 +4,17 @@ import matplotlib as mpl
4
4
  mpl.use("agg")
5
5
  mpl.rcParams["axes.unicode_minus"] = False
6
6
  import matplotlib.pyplot as plt
7
- from pymatgen.io.vasp import Vasprun, BSVasprun
7
+ from pymatgen.io.vasp import BSVasprun
8
8
  from pymatgen.electronic_structure.plotter import BSPlotter
9
9
 
10
+
10
11
  def plot_band_structure(vasprun_path, kpoints_path=None, output="valyte_band.png",
11
12
  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 ---
13
+ """Plot the electronic band structure from a VASP vasprun.xml."""
14
+
15
+ if os.path.isdir(vasprun_path):
16
+ vasprun_path = os.path.join(vasprun_path, "vasprun.xml")
17
+
26
18
  font_map = {
27
19
  "arial": "Arial",
28
20
  "helvetica": "Helvetica",
@@ -37,91 +29,58 @@ def plot_band_structure(vasprun_path, kpoints_path=None, output="valyte_band.png
37
29
  mpl.rcParams["xtick.major.width"] = 1.2
38
30
  mpl.rcParams["ytick.major.width"] = 1.2
39
31
 
40
- # print(f"🔍 Reading {vasprun_path} ...") # Silent mode
41
-
42
32
  try:
43
- # Load VASP output
44
- # BSVasprun is optimized for band structures
45
33
  vr = BSVasprun(vasprun_path, parse_projected_eigen=False)
46
34
  bs = vr.get_band_structure(kpoints_filename=kpoints_path, line_mode=True)
47
35
  except Exception as e:
48
36
  raise ValueError(f"Failed to load band structure: {e}")
49
37
 
50
- # Use BSPlotter to get the data in a plot-friendly format
51
38
  bs_plotter = BSPlotter(bs)
52
39
  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
40
+
41
+ distances = data["distances"]
42
+ energies = data["energy"]
43
+ ticks = data["ticks"]
44
+
60
45
  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
46
+
47
+ color_vb = "#8e44ad"
48
+ color_cb = "#2a9d8f"
49
+
68
50
  for i in range(len(distances)):
69
51
  d = distances[i]
70
-
71
- # Handle different energy data structures
52
+
72
53
  if isinstance(energies, dict):
73
- # Structure: {'1': [seg1, seg2, ...], '-1': ...}
74
- # Iterate over spins
75
54
  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
55
  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
56
+ c = color_vb if np.mean(band) <= 0 else color_cb
84
57
  ax.plot(d, band, color=c, lw=1.5, alpha=1.0)
85
58
  else:
86
- # Structure: [{'1': bands, ...}, {'1': bands, ...}] (List of dicts)
87
- # Iterate over spin channels in this segment
88
59
  for spin in energies[i]:
89
- # energies[i][spin] is a list of arrays (one per band)
90
60
  for band in energies[i][spin]:
91
- if np.mean(band) <= 0:
92
- c = color_vb
93
- else:
94
- c = color_cb
61
+ c = color_vb if np.mean(band) <= 0 else color_cb
95
62
  ax.plot(d, band, color=c, lw=1.5, alpha=1.0)
96
63
 
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']]
64
+ ax.set_xticks(ticks["distance"])
65
+ clean_labels = [(l or "").replace("$\\mid$", "|") for l in ticks["label"]]
101
66
  ax.set_xticklabels(clean_labels, fontsize=14, fontweight="bold")
102
-
103
- # Draw vertical lines at high-symmetry points
104
- for d in ticks['distance']:
67
+
68
+ for d in ticks["distance"]:
105
69
  ax.axvline(d, color="k", lw=0.8, ls="-", alpha=0.3)
106
-
107
- # Draw VBM line (E=0)
70
+
108
71
  ax.axhline(0, color="k", lw=0.8, ls="--", alpha=0.5)
109
72
 
110
- # Setup Y-axis
111
73
  ax.set_ylabel("Energy (eV)", fontsize=16, fontweight="bold", labelpad=8)
112
74
  if ylim:
113
75
  ax.set_ylim(ylim)
114
- # Set y-ticks with 1 eV spacing
115
76
  yticks = np.arange(np.ceil(ylim[0]), np.floor(ylim[1]) + 1, 1)
116
77
  ax.set_yticks(yticks)
117
78
  else:
118
- # Default zoom around gap
119
79
  ax.set_ylim(-4, 4)
120
80
  ax.set_yticks(np.arange(-4, 5, 1))
121
-
81
+
122
82
  ax.set_xlim(distances[0][0], distances[-1][-1])
123
83
 
124
84
  plt.tight_layout()
125
85
  plt.savefig(output, dpi=dpi)
126
86
  plt.close(fig)
127
- # print(f"✅ Band structure saved to {output}") # Silent mode