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 +140 -171
- valyte/band_plot.py +25 -66
- valyte/cli.py +81 -78
- valyte/dos_plot.py +137 -306
- valyte/ipr.py +186 -0
- valyte/kpoints.py +34 -50
- valyte/potcar.py +36 -0
- valyte/supercell.py +5 -22
- {valyte-0.1.8.dist-info → valyte-0.1.11.dist-info}/METADATA +72 -2
- valyte-0.1.11.dist-info/RECORD +19 -0
- valyte-0.1.8.dist-info/RECORD +0 -17
- {valyte-0.1.8.dist-info → valyte-0.1.11.dist-info}/WHEEL +0 -0
- {valyte-0.1.8.dist-info → valyte-0.1.11.dist-info}/entry_points.txt +0 -0
- {valyte-0.1.8.dist-info → valyte-0.1.11.dist-info}/top_level.txt +0 -0
valyte/dos_plot.py
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
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
|
-
"""
|
|
2
|
+
"""DOS plotting utilities."""
|
|
8
3
|
|
|
9
4
|
import os
|
|
10
5
|
import numpy as np
|
|
@@ -19,128 +14,75 @@ from pymatgen.io.vasp import Vasprun
|
|
|
19
14
|
from pymatgen.electronic_structure.core import Spin
|
|
20
15
|
|
|
21
16
|
|
|
22
|
-
# ===============================================================
|
|
23
|
-
# Gradient fill aesthetic
|
|
24
|
-
# ===============================================================
|
|
25
17
|
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
|
-
"""
|
|
18
|
+
"""Fill area under a curve with a vertical gradient."""
|
|
40
19
|
if ax is None:
|
|
41
20
|
ax = plt.gca()
|
|
42
|
-
|
|
43
|
-
# Don't filter by xlim - use full data range for better appearance
|
|
21
|
+
|
|
44
22
|
if len(x) == 0 or len(y) == 0:
|
|
45
23
|
return None
|
|
46
|
-
|
|
47
|
-
# Plot the main line
|
|
24
|
+
|
|
48
25
|
line, = ax.plot(x, y, color=color, lw=2, **kwargs)
|
|
49
|
-
|
|
50
|
-
# Determine fill color and alpha
|
|
26
|
+
|
|
51
27
|
fill_color = line.get_color() if color is None else color
|
|
52
28
|
alpha = line.get_alpha() or 1.0
|
|
53
29
|
zorder = line.get_zorder()
|
|
54
30
|
|
|
55
|
-
# Create a gradient image with more aggressive alpha
|
|
56
31
|
z = np.empty((100, 1, 4))
|
|
57
32
|
rgb = mcolors.to_rgb(fill_color)
|
|
58
33
|
z[:, :, :3] = rgb
|
|
59
|
-
|
|
60
|
-
# Gradient Logic based on relative height
|
|
61
|
-
# We want opacity to be proportional to height relative to the max visible value (ymax_ref)
|
|
62
|
-
|
|
63
|
-
# Create normalized alpha gradient (0 to 1)
|
|
64
|
-
# We map y-values to alpha values.
|
|
65
|
-
# Since imshow fills a rectangle, we create a vertical gradient
|
|
66
|
-
# and clip it later.
|
|
67
|
-
|
|
68
|
-
# Opacity range: 0.05 (at axis) to 0.95 (at max visible height)
|
|
69
|
-
# This ensures "Darker colour at the top"
|
|
34
|
+
|
|
70
35
|
min_alpha = 0.05
|
|
71
36
|
max_alpha = 0.95
|
|
72
|
-
|
|
73
|
-
# Create the gradient array (vertical)
|
|
74
|
-
# 0 is bottom, 1 is top
|
|
75
37
|
gradient_vector = np.linspace(min_alpha, max_alpha, 100)
|
|
76
|
-
|
|
77
|
-
# IMPORTANT: Restore alpha scaling so total DOS (alpha=0.15) stays faint
|
|
78
38
|
gradient_vector *= alpha
|
|
79
|
-
|
|
80
|
-
# If data is negative (Spin Down), we want opaque at bottom (peak) and transparent at top (axis)
|
|
39
|
+
|
|
81
40
|
if np.mean(y) < 0:
|
|
82
41
|
gradient_vector = gradient_vector[::-1]
|
|
83
|
-
|
|
42
|
+
|
|
84
43
|
z[:, :, -1] = gradient_vector[:, None]
|
|
85
|
-
|
|
44
|
+
|
|
86
45
|
xmin, xmax = x.min(), x.max()
|
|
87
|
-
|
|
88
|
-
# Determine extent based on LOCAL curve limits
|
|
89
|
-
# User requested "each curve should have its own gradient"
|
|
90
|
-
# So we scale from 0 to curve.max()
|
|
91
|
-
|
|
46
|
+
|
|
92
47
|
local_ymax = max(y.max(), abs(y.min()))
|
|
93
|
-
if local_ymax == 0:
|
|
94
|
-
|
|
48
|
+
if local_ymax == 0:
|
|
49
|
+
local_ymax = 1.0
|
|
50
|
+
|
|
95
51
|
if np.mean(y) < 0:
|
|
96
|
-
# Spin Down: Extent from -local_ymax to 0
|
|
97
52
|
extent_ymin = -local_ymax
|
|
98
53
|
extent_ymax = 0
|
|
99
54
|
else:
|
|
100
|
-
# Spin Up: Extent from 0 to local_ymax
|
|
101
55
|
extent_ymin = 0
|
|
102
56
|
extent_ymax = local_ymax
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
57
|
+
|
|
58
|
+
im = ax.imshow(
|
|
59
|
+
z,
|
|
60
|
+
aspect="auto",
|
|
61
|
+
extent=[xmin, xmax, extent_ymin, extent_ymax],
|
|
62
|
+
origin="lower",
|
|
63
|
+
zorder=zorder,
|
|
64
|
+
)
|
|
65
|
+
|
|
111
66
|
xy = np.column_stack([x, y])
|
|
112
|
-
|
|
113
|
-
# Construct polygon vertices:
|
|
114
|
-
# Start at (xmin, 0), go along curve (x, y), end at (xmax, 0), close back to start
|
|
115
67
|
verts = np.vstack([[x[0], 0], xy, [x[-1], 0], [x[0], 0]])
|
|
116
|
-
|
|
68
|
+
|
|
117
69
|
clip = Polygon(verts, lw=0, facecolor="none", closed=True)
|
|
118
70
|
ax.add_patch(clip)
|
|
119
71
|
im.set_clip_path(clip)
|
|
120
|
-
|
|
72
|
+
|
|
121
73
|
return line
|
|
122
74
|
|
|
123
75
|
|
|
124
|
-
# ===============================================================
|
|
125
|
-
# Data container
|
|
126
|
-
# ===============================================================
|
|
127
76
|
class ValyteDos:
|
|
128
|
-
"""
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
Attributes:
|
|
132
|
-
energies (np.ndarray): Array of energy values (shifted by Fermi energy).
|
|
133
|
-
densities (dict): Dictionary of {Spin: np.ndarray} for total DOS.
|
|
134
|
-
efermi (float): Fermi energy.
|
|
135
|
-
"""
|
|
77
|
+
"""Container for total DOS data."""
|
|
78
|
+
|
|
136
79
|
def __init__(self, energies, densities, efermi):
|
|
137
80
|
self.energies = np.array(energies)
|
|
138
81
|
self.densities = densities
|
|
139
82
|
self.efermi = float(efermi)
|
|
140
|
-
|
|
83
|
+
|
|
141
84
|
@property
|
|
142
85
|
def total(self):
|
|
143
|
-
"""Returns the sum of all spin channels."""
|
|
144
86
|
tot = np.zeros_like(self.energies)
|
|
145
87
|
for spin in self.densities:
|
|
146
88
|
tot += self.densities[spin]
|
|
@@ -155,144 +97,91 @@ class ValyteDos:
|
|
|
155
97
|
return self.densities.get(Spin.down, np.zeros_like(self.energies))
|
|
156
98
|
|
|
157
99
|
|
|
158
|
-
# ===============================================================
|
|
159
|
-
# Load DOS
|
|
160
|
-
# ===============================================================
|
|
161
100
|
def load_dos(vasprun, elements=None, **_):
|
|
162
|
-
"""
|
|
163
|
-
Loads DOS data from a vasprun.xml file using pymatgen.
|
|
164
|
-
|
|
165
|
-
Args:
|
|
166
|
-
vasprun (str): Path to the vasprun.xml file or directory containing it.
|
|
167
|
-
elements (list or dict, optional): Specific elements to extract PDOS for.
|
|
168
|
-
|
|
169
|
-
Returns:
|
|
170
|
-
tuple: (ValyteDos object, dict of PDOS data)
|
|
171
|
-
"""
|
|
172
|
-
|
|
173
|
-
# Handle directory input
|
|
101
|
+
"""Load total and projected DOS from a vasprun.xml file."""
|
|
174
102
|
if os.path.isdir(vasprun):
|
|
175
103
|
vasprun = os.path.join(vasprun, "vasprun.xml")
|
|
176
|
-
|
|
104
|
+
|
|
177
105
|
if not os.path.exists(vasprun):
|
|
178
106
|
raise FileNotFoundError(f"{vasprun} not found")
|
|
179
107
|
|
|
180
|
-
# Parse VASP output
|
|
181
108
|
vr = Vasprun(vasprun)
|
|
182
109
|
dos = vr.complete_dos
|
|
183
|
-
|
|
184
|
-
# Get Fermi Energy
|
|
185
110
|
efermi = dos.efermi
|
|
186
|
-
|
|
187
|
-
# Attempt to align VBM to 0 for insulators/semiconductors
|
|
111
|
+
|
|
188
112
|
try:
|
|
189
|
-
# Try using BandStructure first (more robust)
|
|
190
113
|
bs = vr.get_band_structure()
|
|
191
114
|
if not bs.is_metal():
|
|
192
115
|
efermi = bs.get_vbm()["energy"]
|
|
193
116
|
except Exception:
|
|
194
|
-
# Fallback to DOS-based detection
|
|
195
117
|
try:
|
|
196
118
|
cbm, vbm = dos.get_cbm_vbm()
|
|
197
|
-
if cbm - vbm > 0.01:
|
|
119
|
+
if cbm - vbm > 0.01:
|
|
198
120
|
efermi = vbm
|
|
199
121
|
except Exception:
|
|
200
122
|
pass
|
|
201
|
-
|
|
202
|
-
# Shift energies to set reference at 0
|
|
203
|
-
energies = dos.energies - efermi
|
|
204
123
|
|
|
205
|
-
|
|
124
|
+
energies = dos.energies - efermi
|
|
206
125
|
pdos = get_pdos(dos, elements)
|
|
207
|
-
|
|
126
|
+
|
|
208
127
|
return ValyteDos(energies, dos.densities, efermi), pdos
|
|
209
128
|
|
|
210
129
|
|
|
211
|
-
# ===============================================================
|
|
212
|
-
# Extract PDOS
|
|
213
|
-
# ===============================================================
|
|
214
130
|
def get_pdos(dos, elements=None):
|
|
215
|
-
"""
|
|
216
|
-
Extracts Projected DOS (PDOS) for specified elements.
|
|
217
|
-
|
|
218
|
-
Args:
|
|
219
|
-
dos (pymatgen.electronic_structure.dos.CompleteDos): The complete DOS object.
|
|
220
|
-
elements (list or dict, optional): Elements to extract. If None, extracts all.
|
|
221
|
-
|
|
222
|
-
Returns:
|
|
223
|
-
dict: A dictionary where keys are element symbols and values are dicts of orbital DOS.
|
|
224
|
-
pdos[element][orbital] = {Spin.up: array, Spin.down: array}
|
|
225
|
-
"""
|
|
131
|
+
"""Extract projected DOS for specified elements."""
|
|
226
132
|
structure = dos.structure
|
|
227
133
|
symbols = [str(site.specie) for site in structure]
|
|
228
|
-
|
|
229
|
-
# If no elements specified, use all unique elements in the structure
|
|
134
|
+
|
|
230
135
|
if not elements:
|
|
231
136
|
unique = sorted(set(symbols))
|
|
232
137
|
elements = {el: () for el in unique}
|
|
233
138
|
else:
|
|
234
|
-
# Ensure elements is a dict if passed as list
|
|
235
139
|
if isinstance(elements, list):
|
|
236
|
-
|
|
140
|
+
elements = {el: () for el in elements}
|
|
237
141
|
|
|
238
142
|
pdos = {}
|
|
239
143
|
for el in elements:
|
|
240
|
-
# Find all sites corresponding to this element
|
|
241
144
|
el_sites = [s for s in structure if str(s.specie) == el]
|
|
242
145
|
el_pdos = {}
|
|
243
|
-
|
|
146
|
+
|
|
244
147
|
for site in el_sites:
|
|
245
148
|
try:
|
|
246
149
|
site_dos = dos.get_site_spd_dos(site)
|
|
247
150
|
except Exception:
|
|
248
151
|
continue
|
|
249
|
-
|
|
250
|
-
# Sum up contributions from orbitals (s, p, d, f)
|
|
152
|
+
|
|
251
153
|
for orb, orb_dos in site_dos.items():
|
|
252
|
-
label = orb.name[0]
|
|
253
|
-
|
|
254
|
-
# Initialize dictionaries for spins if not exists
|
|
154
|
+
label = orb.name[0]
|
|
255
155
|
if label not in el_pdos:
|
|
256
156
|
el_pdos[label] = {}
|
|
257
|
-
|
|
157
|
+
|
|
258
158
|
for spin in orb_dos.densities:
|
|
259
159
|
if spin not in el_pdos[label]:
|
|
260
160
|
el_pdos[label][spin] = np.zeros_like(dos.energies)
|
|
261
161
|
el_pdos[label][spin] += orb_dos.densities[spin]
|
|
262
|
-
|
|
162
|
+
|
|
263
163
|
pdos[el] = el_pdos
|
|
264
164
|
return pdos
|
|
265
165
|
|
|
266
166
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
""
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
dpi (int): Resolution of the output image.
|
|
286
|
-
legend_loc (str): Legend location strategy.
|
|
287
|
-
font (str): Font family to use.
|
|
288
|
-
show_fermi (bool): Whether to draw a dashed line at the Fermi level (E=0).
|
|
289
|
-
show_total (bool): Whether to plot the Total DOS.
|
|
290
|
-
plotting_config (list): List of (Element, Orbital) tuples to plot.
|
|
291
|
-
legend_cutoff (float): Threshold (as fraction) for showing items in legend (default: 0.10).
|
|
292
|
-
scale_factor (float): Factor to scale the Y-axis limits (zoom in).
|
|
293
|
-
"""
|
|
294
|
-
|
|
295
|
-
# --- Font configuration ---
|
|
167
|
+
def plot_dos(
|
|
168
|
+
dos,
|
|
169
|
+
pdos,
|
|
170
|
+
out="valyte_dos.png",
|
|
171
|
+
xlim=(-6, 6),
|
|
172
|
+
ylim=None,
|
|
173
|
+
figsize=(5, 4),
|
|
174
|
+
dpi=400,
|
|
175
|
+
legend_loc="auto",
|
|
176
|
+
font="Arial",
|
|
177
|
+
show_fermi=False,
|
|
178
|
+
show_total=True,
|
|
179
|
+
plotting_config=None,
|
|
180
|
+
legend_cutoff=0.10,
|
|
181
|
+
scale_factor=1.0,
|
|
182
|
+
):
|
|
183
|
+
"""Plot total and projected DOS with the Valyte style."""
|
|
184
|
+
|
|
296
185
|
font_map = {
|
|
297
186
|
"arial": "Arial",
|
|
298
187
|
"helvetica": "Helvetica",
|
|
@@ -307,235 +196,177 @@ def plot_dos(dos, pdos, out="valyte_dos.png",
|
|
|
307
196
|
|
|
308
197
|
plt.style.use("default")
|
|
309
198
|
fig, ax = plt.subplots(figsize=figsize)
|
|
310
|
-
|
|
311
|
-
# Check if spin-polarized
|
|
199
|
+
|
|
312
200
|
is_spin_polarized = Spin.down in dos.densities
|
|
313
|
-
|
|
314
|
-
# Fermi level line (optional)
|
|
201
|
+
|
|
315
202
|
if show_fermi:
|
|
316
203
|
ax.axvline(0, color="k", lw=0.8, ls="--", alpha=0.7)
|
|
317
|
-
|
|
318
|
-
# Zero line for spin polarized plots
|
|
204
|
+
|
|
319
205
|
if is_spin_polarized:
|
|
320
206
|
ax.axhline(0, color="k", lw=0.5, alpha=1.0)
|
|
321
207
|
|
|
322
|
-
# Color palette for elements
|
|
323
|
-
# Expanded color palette for better distinction
|
|
324
|
-
# Reordered to maximize contrast between consecutive items
|
|
325
208
|
palette = [
|
|
326
|
-
"#4b0082",
|
|
327
|
-
"#0096c7",
|
|
328
|
-
"#e63946",
|
|
329
|
-
"#023e8a",
|
|
330
|
-
"#ffb703",
|
|
331
|
-
"#2a9d8f",
|
|
332
|
-
"#8e44ad",
|
|
333
|
-
"#118ab2",
|
|
334
|
-
"#d62828",
|
|
335
|
-
"#00b4d8",
|
|
336
|
-
"#f4a261",
|
|
337
|
-
"#003049",
|
|
338
|
-
"#6a994e",
|
|
339
|
-
"#48cae4",
|
|
340
|
-
"#0077b6",
|
|
341
|
-
"#90e0ef",
|
|
342
|
-
"#ade8f4",
|
|
343
|
-
"#caf0f8"
|
|
209
|
+
"#4b0082",
|
|
210
|
+
"#0096c7",
|
|
211
|
+
"#e63946",
|
|
212
|
+
"#023e8a",
|
|
213
|
+
"#ffb703",
|
|
214
|
+
"#2a9d8f",
|
|
215
|
+
"#8e44ad",
|
|
216
|
+
"#118ab2",
|
|
217
|
+
"#d62828",
|
|
218
|
+
"#00b4d8",
|
|
219
|
+
"#f4a261",
|
|
220
|
+
"#003049",
|
|
221
|
+
"#6a994e",
|
|
222
|
+
"#48cae4",
|
|
223
|
+
"#0077b6",
|
|
224
|
+
"#90e0ef",
|
|
225
|
+
"#ade8f4",
|
|
226
|
+
"#caf0f8",
|
|
344
227
|
]
|
|
345
228
|
lines, labels = [], []
|
|
346
|
-
|
|
347
|
-
# Determine mask for visible x-range (used for scaling and legend)
|
|
229
|
+
|
|
348
230
|
x_mask = (dos.energies >= xlim[0]) & (dos.energies <= xlim[1])
|
|
349
231
|
|
|
350
|
-
# Determine what to plot
|
|
351
232
|
if plotting_config:
|
|
352
233
|
items_to_plot = plotting_config
|
|
353
234
|
else:
|
|
354
|
-
# Default: Plot all orbitals for each loaded element
|
|
355
235
|
items_to_plot = []
|
|
356
236
|
for el, el_pdos in pdos.items():
|
|
357
237
|
for orb in el_pdos.keys():
|
|
358
238
|
items_to_plot.append((el, orb))
|
|
359
239
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
240
|
+
max_visible_y = 0
|
|
241
|
+
min_visible_y = 0
|
|
242
|
+
|
|
364
243
|
for i, (el, orb) in enumerate(items_to_plot):
|
|
365
244
|
if el not in pdos:
|
|
366
245
|
continue
|
|
367
|
-
|
|
368
|
-
# Assign unique color for each orbital contribution
|
|
246
|
+
|
|
369
247
|
c = palette[i % len(palette)]
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
# y_data_up and y_data_down
|
|
373
|
-
|
|
374
|
-
if orb == 'total':
|
|
375
|
-
# Sum all orbitals for this element
|
|
248
|
+
|
|
249
|
+
if orb == "total":
|
|
376
250
|
y_up = np.zeros_like(dos.energies)
|
|
377
251
|
y_down = np.zeros_like(dos.energies)
|
|
378
|
-
|
|
379
252
|
for o_data in pdos[el].values():
|
|
380
253
|
y_up += o_data.get(Spin.up, np.zeros_like(dos.energies))
|
|
381
254
|
y_down += o_data.get(Spin.down, np.zeros_like(dos.energies))
|
|
382
|
-
|
|
383
255
|
label = el
|
|
384
256
|
else:
|
|
385
|
-
if orb in pdos[el]:
|
|
386
|
-
y_up = pdos[el][orb].get(Spin.up, np.zeros_like(dos.energies))
|
|
387
|
-
y_down = pdos[el][orb].get(Spin.down, np.zeros_like(dos.energies))
|
|
388
|
-
label = f"{el}({orb})"
|
|
389
|
-
else:
|
|
257
|
+
if orb not in pdos[el]:
|
|
390
258
|
continue
|
|
391
|
-
|
|
392
|
-
|
|
259
|
+
y_up = pdos[el][orb].get(Spin.up, np.zeros_like(dos.energies))
|
|
260
|
+
y_down = pdos[el][orb].get(Spin.down, np.zeros_like(dos.energies))
|
|
261
|
+
label = f"{el}({orb})"
|
|
262
|
+
|
|
393
263
|
y_down = -y_down
|
|
394
|
-
|
|
395
|
-
# Check contribution in visible range
|
|
264
|
+
|
|
396
265
|
visible_y_up = y_up[x_mask]
|
|
397
266
|
visible_y_down = y_down[x_mask]
|
|
398
|
-
|
|
267
|
+
|
|
399
268
|
has_visible_data = False
|
|
400
269
|
current_max_y = 0
|
|
401
|
-
|
|
270
|
+
|
|
402
271
|
if len(visible_y_up) > 0:
|
|
403
272
|
max_y = np.max(visible_y_up)
|
|
404
273
|
max_visible_y = max(max_visible_y, max_y)
|
|
405
274
|
current_max_y = max(current_max_y, max_y)
|
|
406
|
-
if max_y > 1e-6:
|
|
407
|
-
|
|
275
|
+
if max_y > 1e-6:
|
|
276
|
+
has_visible_data = True
|
|
277
|
+
|
|
408
278
|
if is_spin_polarized and len(visible_y_down) > 0:
|
|
409
279
|
min_y = np.min(visible_y_down)
|
|
410
280
|
min_visible_y = min(min_visible_y, min_y)
|
|
411
281
|
current_max_y = max(current_max_y, abs(min_y))
|
|
412
|
-
if abs(min_y) > 1e-6:
|
|
282
|
+
if abs(min_y) > 1e-6:
|
|
283
|
+
has_visible_data = True
|
|
413
284
|
|
|
414
|
-
# Store for later threshold check and plotting
|
|
415
|
-
# We plot a dummy line for the legend
|
|
416
285
|
line, = ax.plot(dos.energies, y_up, lw=1.5, color=c, label=label, alpha=0)
|
|
417
|
-
lines.append(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
286
|
+
lines.append(
|
|
287
|
+
{
|
|
288
|
+
"line": line,
|
|
289
|
+
"y_up": y_up,
|
|
290
|
+
"y_down": y_down,
|
|
291
|
+
"max_y": current_max_y,
|
|
292
|
+
"color": c,
|
|
293
|
+
"label": label,
|
|
294
|
+
"has_visible": has_visible_data,
|
|
295
|
+
}
|
|
296
|
+
)
|
|
297
|
+
|
|
428
298
|
global_max = max(max_visible_y, abs(min_visible_y))
|
|
429
299
|
threshold = legend_cutoff * global_max
|
|
430
|
-
|
|
431
|
-
# Auto-scale Y-axis calculation (to determine ymax_ref)
|
|
432
|
-
if ylim:
|
|
433
|
-
pass # ymax_ref = ylim[1] (Unused)
|
|
434
|
-
else:
|
|
435
|
-
# Determine likely ymax based on logic later in the function
|
|
436
|
-
pass
|
|
437
300
|
|
|
438
|
-
# Filter legend items but keep all plot lines
|
|
439
301
|
final_lines = []
|
|
440
302
|
final_labels = []
|
|
441
|
-
|
|
303
|
+
|
|
442
304
|
for item in lines:
|
|
443
|
-
line = item[
|
|
444
|
-
y_up = item[
|
|
445
|
-
y_down = item[
|
|
446
|
-
c = item[
|
|
447
|
-
label = item[
|
|
448
|
-
max_y = item[
|
|
449
|
-
|
|
450
|
-
|
|
305
|
+
line = item["line"]
|
|
306
|
+
y_up = item["y_up"]
|
|
307
|
+
y_down = item["y_down"]
|
|
308
|
+
c = item["color"]
|
|
309
|
+
label = item["label"]
|
|
310
|
+
max_y = item["max_y"]
|
|
311
|
+
has_visible = item["has_visible"]
|
|
312
|
+
|
|
451
313
|
line.set_alpha(1.0)
|
|
452
|
-
|
|
453
|
-
# Apply gradient fill for visible lines
|
|
454
|
-
# Spin Up
|
|
314
|
+
|
|
455
315
|
gradient_fill(dos.energies, y_up, ax=ax, color=c, alpha=0.9)
|
|
456
|
-
|
|
457
|
-
# Spin Down
|
|
458
316
|
if is_spin_polarized:
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if max_y >= threshold:
|
|
317
|
+
gradient_fill(dos.energies, y_down, ax=ax, color=c, alpha=0.9)
|
|
318
|
+
|
|
319
|
+
if has_visible and max_y >= threshold:
|
|
463
320
|
final_lines.append(line)
|
|
464
321
|
final_labels.append(label)
|
|
465
|
-
|
|
466
|
-
# Update lines and labels for legend creation
|
|
322
|
+
|
|
467
323
|
lines = final_lines
|
|
468
324
|
labels = final_labels
|
|
469
325
|
|
|
470
|
-
# Plot Total DOS
|
|
471
326
|
if show_total:
|
|
472
327
|
y_total_up = dos.spin_up
|
|
473
328
|
y_total_down = -dos.spin_down
|
|
474
|
-
|
|
329
|
+
|
|
475
330
|
ax.plot(dos.energies, y_total_up, color="k", lw=1.2, label="Total DOS")
|
|
476
|
-
# Use ymax_ref here too, assuming we want scaling relative to frame too
|
|
477
|
-
# But Total DOS is often much larger. Maybe keep it separate max?
|
|
478
|
-
# User said "Maximum value after scaling". So consistent reference is good.
|
|
479
331
|
gradient_fill(dos.energies, y_total_up, ax=ax, color="k", alpha=0.15)
|
|
480
|
-
|
|
332
|
+
|
|
481
333
|
if is_spin_polarized:
|
|
482
334
|
ax.plot(dos.energies, y_total_down, color="k", lw=1.2)
|
|
483
335
|
gradient_fill(dos.energies, y_total_down, ax=ax, color="k", alpha=0.15)
|
|
484
|
-
|
|
485
|
-
# Update max/min range for auto-scaling
|
|
336
|
+
|
|
486
337
|
visible_total_up = y_total_up[x_mask]
|
|
487
338
|
visible_total_down = y_total_down[x_mask]
|
|
488
339
|
if len(visible_total_up) > 0:
|
|
489
340
|
max_visible_y = max(max_visible_y, np.max(visible_total_up))
|
|
490
341
|
if len(visible_total_down) > 0:
|
|
491
342
|
min_visible_y = min(min_visible_y, np.min(visible_total_down))
|
|
492
|
-
|
|
493
|
-
# Auto-scale Y-axis based on visible range if ylim not provided
|
|
343
|
+
|
|
494
344
|
if not ylim:
|
|
495
345
|
if max_visible_y > 0 or min_visible_y < 0:
|
|
496
|
-
# Apply scaling factor to the limit (zoom in)
|
|
497
346
|
upper_limit = (max_visible_y * 1.1) / scale_factor
|
|
498
347
|
lower_limit = (min_visible_y * 1.1) / scale_factor if is_spin_polarized else 0
|
|
499
|
-
|
|
500
|
-
# If spin polarized, maybe make symmetric if user wants?
|
|
501
|
-
# For now, let's just use the data range.
|
|
502
|
-
# But often symmetric is nicer. Let's stick to data range for now.
|
|
503
|
-
|
|
504
348
|
ax.set_ylim(lower_limit, upper_limit)
|
|
505
349
|
else:
|
|
506
350
|
ax.set_ylim(*ylim)
|
|
507
351
|
|
|
508
|
-
# Axis settings
|
|
509
352
|
ax.set_xlim(*xlim)
|
|
510
353
|
ax.set_xlabel("Energy (eV)", fontsize=14, weight="bold", labelpad=6)
|
|
511
354
|
ax.set_ylabel("Density of States", fontsize=14, weight="bold", labelpad=6)
|
|
512
|
-
|
|
513
|
-
# Set x-ticks with 1 eV spacing
|
|
355
|
+
|
|
514
356
|
xticks = np.arange(np.ceil(xlim[0]), np.floor(xlim[1]) + 1, 1)
|
|
515
357
|
ax.set_xticks(xticks)
|
|
516
|
-
|
|
517
|
-
tick_labels = [f'{int(x)}' if x == int(x) else f'{x}' for x in xticks]
|
|
358
|
+
tick_labels = [f"{int(x)}" if x == int(x) else f"{x}" for x in xticks]
|
|
518
359
|
ax.set_xticklabels(tick_labels, fontweight="bold")
|
|
519
360
|
ax.set_yticks([])
|
|
520
361
|
|
|
521
|
-
|
|
522
|
-
# Only show legend if there are items to display
|
|
523
|
-
show_legend = len(lines) > 0
|
|
524
|
-
|
|
525
|
-
if show_legend:
|
|
526
|
-
# Check for overlap to decide legend position
|
|
527
|
-
# Simplified overlap check for now
|
|
528
|
-
loc, ncol = "upper right", 1
|
|
529
|
-
|
|
530
|
-
# If spin polarized, upper right might cover spin up data
|
|
531
|
-
# But usually it's fine.
|
|
532
|
-
|
|
362
|
+
if len(lines) > 0:
|
|
533
363
|
legend = ax.legend(
|
|
534
|
-
lines,
|
|
364
|
+
lines,
|
|
365
|
+
labels,
|
|
535
366
|
frameon=False,
|
|
536
367
|
fontsize=13,
|
|
537
|
-
loc=
|
|
538
|
-
ncol=
|
|
368
|
+
loc="upper right" if legend_loc == "auto" else legend_loc,
|
|
369
|
+
ncol=1,
|
|
539
370
|
handlelength=1.5,
|
|
540
371
|
columnspacing=0.8,
|
|
541
372
|
handletextpad=0.6,
|