Coverage for gpaw/cli/dos.py: 91%
91 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-14 00:18 +0000
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-14 00:18 +0000
1"""CLI-code for dos-subcommand."""
2from pathlib import Path
3from typing import Union, List, Tuple, Optional
5import numpy as np
7from gpaw import GPAW
8from gpaw.setup import Setup
9from gpaw.spherical_harmonics import names as ylmnames
10from gpaw.dos import DOSCalculator
13class CLICommand:
14 """Calculate (projected) density of states from gpw-file."""
16 @staticmethod
17 def add_arguments(parser):
18 add = parser.add_argument
19 add('gpw', metavar='gpw-file')
20 add('-p', '--plot', action='store_true',
21 help='Plot the DOS.')
22 add('-i', '--integrated', action='store_true',
23 help='Calculate integrated DOS.')
24 add('-w', '--width', type=float, default=0.1,
25 help='Width of Gaussian. Use 0.0 to use linear tetrahedron '
26 'interpolation.')
27 add('-a', '--atom', help='Project onto atoms: "Cu-spd,H-s" or use '
28 'atom indices "12-spdf". Particular m-values can be obtained '
29 'like this: "N-p0,N-p1,N-p2. For p-orbitals, m=0,1,2 translates '
30 'to y, z and x. For d-orbitals, m=0,1,2,3,4 translates '
31 'to xy, yz, 3z2-r2, zx and x2-y2.')
32 add('-t', '--total', action='store_true',
33 help='Show both PDOS and total DOS.')
34 add('-r', '--range', nargs=2, metavar=('emin', 'emax'),
35 help='Energy range in eV relative to Fermi level.')
36 add('-n', '--points', type=int, default=400, help='Number of points.')
37 add('--soc', action='store_true',
38 help='Include spin-orbit coupling.')
40 @staticmethod
41 def run(args):
42 if args.range is None:
43 emin = None
44 emax = None
45 else:
46 emin, emax = (float(e) for e in args.range)
47 dos(args.gpw,
48 plot=args.plot,
49 width=args.width,
50 integrated=args.integrated,
51 projection=args.atom,
52 soc=args.soc,
53 emin=emin,
54 emax=emax,
55 npoints=args.points,
56 show_total=args.total)
59def parse_projection_string(projection: str,
60 symbols: List[str],
61 setups: List[Setup]
62 ) -> List[Tuple[str,
63 List[Tuple[int,
64 int,
65 Union[None, int]]]]]:
66 """Create labels and lists of (a, l, m)-tuples.
68 Example:
70 >>> from gpaw.setup import create_setup
71 >>> setup = create_setup('Li')
72 >>> s, py = parse_projection_string('Li-sp0',
73 ... ['Li', 'Li'],
74 ... [setup, setup])
75 >>> s
76 ('Li-s', [(0, 0, None), (1, 0, None)])
77 >>> py
78 ('Li-p(y)', [(0, 1, 0), (1, 1, 0)])
80 * "Li-s" will have contributions from l=0 and atoms 0 and 1
81 * "Li-p(y)" will have contributions from l=1, m=0 and atoms 0 and 1
83 """
84 result: List[Tuple[str, List[Tuple[int, int, Union[None, int]]]]] = []
85 for proj in projection.split(','):
86 s, ll = proj.split('-')
87 if s.isdigit():
88 A = [int(s)]
89 s = '#' + s
90 else:
91 A = [a for a, symbol in
92 enumerate(symbols)
93 if symbol == s]
94 if not A:
95 raise ValueError('No such atom: ' + s)
96 for l, m in parse_lm_string(ll):
97 label = s + '-' + 'spdfg'[l]
98 if m is not None:
99 name = ylmnames[l][m]
100 label += f'({name})'
101 result.append((label, [(a, l, m) for a in A]))
103 return result
106def parse_lm_string(s: str) -> List[Tuple[int, Union[None, int]]]:
107 """Parse 'spdf' kind of string to numbers.
109 Return list of (l, m) tuples with m=None if not specified:
111 >>> parse_lm_string('sp')
112 [(0, None), (1, None)]
113 >>> parse_lm_string('p0p1p2')
114 [(1, 0), (1, 1), (1, 2)]
115 """
116 result = []
117 while s:
118 l = 'spdfg'.index(s[0])
119 m: Union[None, int]
120 if s[1:2].isnumeric():
121 m = int(s[1:2])
122 s = s[2:]
123 else:
124 m = None
125 s = s[1:]
126 result.append((l, m))
127 return result
130def dos(filename: Union[Path, str],
131 *,
132 plot=False,
133 width=0.1,
134 integrated=False,
135 projection=None,
136 soc=False,
137 emin=None,
138 emax=None,
139 npoints=200,
140 show_total=None):
141 """Calculate density of states.
143 filename: str
144 Name of restart-file.
145 plot: bool
146 Show a plot.
147 width: float
148 Width of Gaussians.
149 integrated: bool
150 Calculate integrated DOS.
152 """
153 from ase.spectrum.dosdata import GridDOSData
154 from ase.spectrum.doscollection import GridDOSCollection
156 calc = GPAW(filename)
158 doscalc = DOSCalculator.from_calculator(calc, soc)
159 energies = doscalc.get_energies(emin, emax, npoints)
160 nspins = doscalc.nspins
161 spinlabels = [''] if nspins == 1 else [' up', ' dn']
162 spins: List[Optional[int]] = [None] if nspins == 1 else [0, 1]
164 dosobjs = GridDOSCollection([], energies)
166 # Note: ignoring wrong GridDOSCollection.__add__ hint below.
168 if projection is None or show_total:
169 for spin, label in zip(spins, spinlabels):
170 dos = doscalc.raw_dos(energies, spin=spin, width=width)
171 dosobjs += GridDOSData(energies, dos, # type: ignore
172 {'label': 'DOS' + label})
174 if projection is not None:
175 symbols = calc.atoms.get_chemical_symbols()
176 projs = parse_projection_string(
177 projection, symbols, calc.setups)
178 for label, contributions in projs:
179 for spin, spinlabel in zip(spins, spinlabels):
180 dos = np.zeros_like(energies)
181 for a, l, m in contributions:
182 dos += doscalc.raw_pdos(energies,
183 a, l, m,
184 spin=spin,
185 width=width)
186 dosobjs += GridDOSData(energies, dos, # type: ignore
187 {'label': label + spinlabel})
189 if integrated:
190 de = energies[1] - energies[0]
191 energies = energies + de / 2
192 dosobjs = GridDOSCollection(
193 [GridDOSData(energies,
194 np.cumsum(obj.get_weights()) * de,
195 obj.info)
196 for obj in dosobjs])
197 ylabel = 'iDOS [electrons]'
198 else:
199 ylabel = 'DOS [electrons/eV]'
201 if plot:
202 ax = dosobjs.plot()
203 ax.set_xlabel(r'$\epsilon-\epsilon_F$ [eV]')
204 ax.set_ylabel(ylabel)
205 import matplotlib.pyplot as plt
206 plt.show()
208 return dosobjs