madman.helpers.ase.workflows.edos
Electron density of states.
1r"""Electron density of states.""" 2 3import os 4from argparse import ArgumentParser 5from collections.abc import Mapping 6from functools import cache 7from math import ceil 8from typing import Any, Optional, Union 9 10import numpy as np 11import seaborn as sns 12import xarray as xr 13import yaml 14from ase.parallel import paropen, parprint, world 15from gpaw.calculator import GPAW 16from matplotlib import pyplot as plt 17from pydantic import BaseModel, Field, confloat, conint, constr, field_validator 18 19from madman.helpers.ase.workflows.converge import MultivariateConvergence 20from madman.utilities import gen_regular_grid, redirect_to 21 22 23sns.set_theme( 24 context="talk", 25 style="white", 26 rc={"figure.titlesize": "medium", "axes.formatter.useoffset": False}, 27) 28 29 30atm_ssh_seq: list[str, str, str, str] = ["s", "p", "d", "f"] 31r"""Atomic subshells (index = l quantum number).""" 32 33 34atm_ssh_to_harm_poly_seq: dict[str, list[str, ...]] = { 35 "s": [""], 36 "p": ["y", "z", "x"], 37 "d": ["xy", "yz", "z2", "xz", "x2-y2"], 38 "f": ["y3", "xyz", "yz2", "z3", "xz2", "x2z-y2z", "x3"], 39} 40r"""Atomic subshells to harmonic polynomials (index = GPAW m quantum number).""" 41 42 43class kptdenConvergenceSettings(BaseModel): 44 r"""$\mathbf{k}$-point density convergence settings.""" 45 46 value_min: confloat(gt=0.0, allow_inf_nan=False) = Field(frozen=True) 47 r"""Minimum value [$\mathrm{Å}$].""" 48 49 value_max: Optional[confloat(gt=0.0, allow_inf_nan=False)] = Field( 50 None, frozen=True 51 ) 52 r"""Maximum value [$\mathrm{Å}$].""" 53 54 value_spc: Optional[confloat(gt=0.0, allow_inf_nan=False)] = Field( 55 None, frozen=True 56 ) 57 r"""Value spacing [$\mathrm{Å}$].""" 58 59 threshold: Optional[confloat(gt=0.0, allow_inf_nan=False)] = Field( 60 None, frozen=True 61 ) 62 r"""Convergence threshold [$\mathrm{eV^{-1} \, Å^{-1}}$].""" 63 64 stability: Optional[conint(gt=0)] = Field(None, frozen=True) 65 r"""Convergence stability.""" 66 67 68class nbandsConvergenceSettings(BaseModel): 69 r"""Number of empty bands convergence settings.""" 70 71 value_min: conint(ge=0) = Field(frozen=True) 72 r"""Minimum value.""" 73 74 value_max: Optional[conint(ge=0)] = Field(None, frozen=True) 75 r"""Maximum value.""" 76 77 value_spc: Optional[conint(gt=0)] = Field(None, frozen=True) 78 r"""Value spacing.""" 79 80 threshold: Optional[confloat(gt=0.0, allow_inf_nan=False)] = Field( 81 None, frozen=True 82 ) 83 r"""Convergence threshold [$\mathrm{eV^{-1}}$].""" 84 85 stability: Optional[conint(gt=0)] = Field(None, frozen=True) 86 r"""Convergence stability.""" 87 88 89class Config(BaseModel): 90 r"""Configuration.""" 91 92 ground_path: constr(pattern=r".*\.gpw$") = Field(frozen=True) 93 r"""Path to electronic ground state.""" 94 95 kptden: Union[ 96 kptdenConvergenceSettings, 97 Mapping, 98 confloat(gt=0.0, allow_inf_nan=False), 99 ] = Field(10.0, frozen=True) 100 r"""$\mathbf{k}$-point density [$\mathrm{Å}$] convergence settings.""" 101 102 nbands_empty: Union[ 103 nbandsConvergenceSettings, 104 Mapping, 105 conint(ge=0), 106 ] = Field(0, frozen=True) 107 r"""Number of empty bands convergence settings.""" 108 109 strict: bool = Field(False, frozen=True) 110 r"""If True, max. slope is used for convergence; otherwise, avg. is used.""" 111 112 niter_max: conint(gt=0) = Field(1, frozen=True) 113 r"""Maximum number of iterations for convergence.""" 114 115 energy_min: confloat(lt=0.0, allow_inf_nan=False) = Field(-5.0, frozen=True) 116 r"""Electron energy minimum w.r.t. Fermi level [$\mathrm{eV}$].""" 117 118 energy_max: confloat(gt=0.0, allow_inf_nan=False) = Field(5.0, frozen=True) 119 r"""Electron energy maximum w.r.t. Fermi level [$\mathrm{eV}$].""" 120 121 energy_spc: confloat(gt=0.0, allow_inf_nan=False) = Field(0.01, frozen=True) 122 r"""Electron energy spacing [$\mathrm{eV}$].""" 123 124 energy_spc_prj: Optional[confloat(gt=0.0, allow_inf_nan=False)] = Field(None, frozen=True) 125 r"""Electron energy spacing for projected density of states.""" 126 127 projected: bool = Field(False, frozen=True) 128 r"""If True, also projected electron density of states is calculated.""" 129 130 gpaw_calc_prefix: Optional[str] = Field(None, frozen=True) 131 r"""Prefix to save GPAW calculator.""" 132 133 edos_save_prefix: Optional[str] = Field(None, frozen=True) 134 r"""Prefix to save electron density of states.""" 135 136 edos_plot_prefix: Optional[str] = Field(None, frozen=True) 137 r"""Prefix to save electron density of states plots.""" 138 139 conv_plot_root: Optional[str] = Field(None, frozen=True) 140 r"""Root to save convergence plots.""" 141 142 log_prefix: Optional[str] = Field(None, frozen=True) 143 r"""Prefix to save GPAW log.""" 144 145 cache_root: Optional[str] = Field(None, frozen=True) 146 r"""Root to cache electron densitysity of states.""" 147 148 parallel: Optional[Mapping] = Field(None, frozen=True) 149 r"""GPAW parallel options.""" 150 151 @field_validator("ground_path") 152 def validate_ground_path(cls, value: str) -> str: 153 r"""Validate `ground_path`. 154 155 Args: 156 value: Path to load electronic ground state. 157 158 Returns: 159 Path to load electronic ground state. 160 161 Raises: 162 FileNotFoundError: If `value` is not a path to an existing file. 163 """ 164 if not os.path.isfile(value): 165 raise FileNotFoundError 166 return value 167 168 @field_validator("kptden") 169 def parse_kptden_map(cls, value: Any) -> Any: 170 r"""Parse $\mathrm{k}$-point density convergence settings map.""" 171 if isinstance(value, Mapping): 172 value = kptdenConvergenceSettings(**value) 173 return value 174 175 @field_validator("nbands_empty") 176 def parse_nbands_empty_map(cls, value: Any) -> Any: 177 r"""Parse number of empty bands convergence settings map.""" 178 if isinstance(value, Mapping): 179 value = nbandsConvergenceSettings(**value) 180 return value 181 182 @field_validator( 183 "gpaw_calc_prefix", 184 "edos_save_prefix", 185 "edos_plot_prefix", 186 "conv_plot_root", 187 "log_prefix", 188 "cache_root", 189 ) 190 def create_tree(cls, value: Optional[str]) -> Optional[str]: 191 r"""Create tree for prefix or path. 192 193 Args: 194 value: Path or prefix of file to save. 195 196 Returns: 197 Path or prefix of file to save. 198 """ 199 if value: 200 tree = os.path.dirname(value) 201 if tree: 202 os.makedirs(tree, exist_ok=True) 203 return value 204 205 206def calc_edos(config: Config) -> None: 207 r"""Calculate electron densitysity of states. 208 209 Args: 210 config: Configuration. 211 """ 212 with redirect_to(config.log_prefix, mode="w"): 213 pass 214 parprint("GPAW log initialized...") 215 216 calc_cache = {} 217 calc_iter_i = 0 218 parprint("GPAW calculator cache initialized...") 219 220 ground = GPAW(config.ground_path) 221 parprint("Electronic ground state imported...") 222 223 nelectrons = ground.get_number_of_electrons() 224 nbands_occupied = ceil(0.5 * nelectrons) 225 parprint(f"Number of occupied bands: {nbands_occupied}...") 226 227 kptden_d = config.kptden 228 if isinstance(kptden_d, float): 229 kptden_d = {"value_min": kptden_d} 230 else: 231 kptden_d = kptden_d.dict() 232 kptden_d.update( 233 { 234 "direction": 1, 235 "criterion": f"slope-abs-{"max" if config.strict else "avg"}", 236 "symb": r"\lambda_{\mathbf{k}}", 237 "unit": r"Å", 238 } 239 ) 240 nbands_empty_d = config.nbands_empty 241 if isinstance(nbands_empty_d, int): 242 nbands_empty_d = {"value_min": nbands_empty_d} 243 else: 244 nbands_empty_d = nbands_empty_d.dict() 245 nbands_empty_d.update( 246 { 247 "direction": 1, 248 "criterion": f"slope-abs-{"max" if config.strict else "avg"}", 249 "symb": r"N^{*}", 250 "unit": r"1", 251 } 252 ) 253 params = {"kptden": kptden_d, "nbands_empty": nbands_empty_d} 254 parprint("Electron density of states convergence parameters set up...") 255 256 energy_grid = gen_regular_grid( 257 config.energy_min, config.energy_max, config.energy_spc 258 ) 259 parprint("Electron energy grid set up...") 260 261 @cache 262 def objective(kptden, nbands_empty): 263 nonlocal calc_iter_i 264 nbands_empty = int(nbands_empty) 265 nbands = nbands_occupied + nbands_empty 266 with redirect_to(config.log_prefix, mode="a"): 267 calc = ground.fixed_density( 268 kpts={"density": kptden, "gamma": True}, 269 nbands=nbands + nbands_empty, 270 convergence={"bands": nbands}, 271 parallel=config.parallel, 272 ) 273 calc_iter_i += 1 274 key = [("kptden", kptden), ("nbands_empty", nbands_empty)] 275 if config.cache_root is None: 276 calc_cache.update({tuple(sorted(key)): calc}) 277 else: 278 calc_path = os.path.join(config.cache_root, f"{calc_iter_i}.gpw") 279 calc.write(calc_path) 280 calc_cache.update({tuple(sorted(key)): calc_path}) 281 edos_calc = calc.dos() 282 edos = edos_calc.raw_dos(energy_grid, width=0.0) 283 return edos 284 285 parprint("Electron density of states objective function set up...") 286 287 convergence = MultivariateConvergence( 288 objective, 289 params, 290 crop=True, 291 req_sc=True, 292 niter_max=config.niter_max, 293 obj_symb=r"\text{Electron DOS}", 294 obj_unit=r"1/eV", 295 ) 296 parprint("Electron density of states convergence set up...") 297 298 parprint("Electron density of states convergence started...") 299 convergence.run() 300 301 if config.conv_plot_root is not None: 302 value_plots_map = convergence.plot("obj-value") 303 slope_plots_map = convergence.plot("obj-slope") 304 305 ndigits = len(str(config.niter_max)) 306 for qnty, plots_map in zip( 307 ["edos", "medos"], [value_plots_map, slope_plots_map] 308 ): 309 for param, plots_seq in plots_map.items(): 310 figdir = os.path.join( 311 config.conv_plot_root, f"{qnty}-vs-{param}" 312 ) 313 os.makedirs(figdir, exist_ok=True) 314 for i, plots in enumerate(plots_seq): 315 (plot,) = tuple(plots) 316 figpath = os.path.join(figdir, f"{i:0{ndigits}.0f}.svg") 317 fig = plot.get_figure() 318 fig.savefig(figpath) 319 plt.close(fig) 320 parprint("Converge plots saved...") 321 322 if convergence.converged is False: 323 parprint("Electron density of states convergence failed!") 324 else: 325 parprint("Electron density of states converged...") 326 327 params_opt = convergence.values_opt 328 key = tuple(sorted(params_opt.items())) 329 if key not in calc_cache: 330 objective(**params_opt) 331 332 if config.cache_root is None: 333 calc = calc_cache[key] 334 else: 335 calc = GPAW(calc_cache[key]) 336 if config.gpaw_calc_prefix is not None: 337 calc.write(f"{config.gpaw_calc_prefix}.gpw") 338 parprint("GPAW calculator saved...") 339 340 edos_calc = calc.dos() 341 edos = edos_calc.raw_dos(energy_grid, width=0.0) 342 parprint("Electron density of states calculated...") 343 344 edos_da = xr.DataArray(edos, coords=[energy_grid], dims=["energy"]) 345 edos_da.to_netcdf(f"{config.edos_save_prefix}_total.nc") 346 parprint("Electron density of states saved...") 347 348 fig, ax = plt.subplots(tight_layout=True) 349 ax.set_xlabel(r"$\epsilon_{\text{KS}}$ / $\mathrm{eV}$") 350 ax.set_ylabel(r"$\text{Electron DOS}$ / $\mathrm{eV^{-1}}$") 351 ax.plot(edos_da.energy.values, edos_da.values, color="k") 352 if config.edos_save_prefix is not None: 353 fig.savefig(f"{config.edos_plot_prefix}_total.svg") 354 parprint("Electron density of states plot saved...") 355 356 if config.projected is True: 357 if config.energy_spc_prj is not None: 358 energy_grid = gen_regular_grid( 359 config.energy_min, config.energy_max, config.energy_spc_prj 360 ) 361 parprint("Calculating projected electron density of states...") 362 edos_proj_da_seq = [] 363 atm_spc_seq = calc.atoms.get_chemical_symbols() 364 for atm_i, atm_spc in enumerate(atm_spc_seq): 365 for atm_ssh in atm_ssh_seq: 366 for harm_poly in sum(atm_ssh_to_harm_poly_seq.values(), []): 367 lqntnum = atm_ssh_seq.index(atm_ssh) 368 try: 369 mqntnum = atm_ssh_to_harm_poly_seq[atm_ssh].index( 370 harm_poly 371 ) 372 edos_proj = edos_calc.raw_pdos( 373 energy_grid, 374 a=atm_i, 375 l=lqntnum, 376 m=mqntnum, 377 width=0.0, 378 ) 379 except ValueError: 380 edos_proj = np.zeros(energy_grid.shape) 381 edos_proj_da = xr.DataArray( 382 edos_proj, coords=[energy_grid], dims=["energy"] 383 ) 384 edos_proj_da = ( 385 edos_proj_da.assign_coords(atm_spc=atm_spc) 386 .expand_dims("atm_spc") 387 .assign_coords(atm_i=atm_i) 388 .expand_dims("atm_i") 389 .assign_coords(atm_ssh=atm_ssh) 390 .expand_dims("atm_ssh") 391 .assign_coords(harm_poly=harm_poly) 392 .expand_dims("harm_poly") 393 ) 394 edos_proj_da_seq.append(edos_proj_da) 395 parprint("Projected electron density of states calculated...") 396 397 edos_proj_da = xr.combine_by_coords(edos_proj_da_seq) 398 if config.edos_save_prefix is not None: 399 edos_proj_da.to_netcdf(f"{config.edos_save_prefix}_projected.nc") 400 parprint("Projected electron density of states saved...") 401 402 edos_proj_vs_atm_spc_da = edos_proj_da.groupby("atm_spc").sum( 403 dim=["atm_i", "atm_ssh", "harm_poly"] 404 ) 405 fig, ax = plt.subplots(tight_layout=True) 406 edos_proj_vs_atm_spc_da.plot(x="energy", hue="atm_spc", ax=ax) 407 ax.set_title("") 408 ax.set_xlabel(r"$\epsilon_{\text{KS}}$ / $\mathrm{eV}$") 409 ax.set_ylabel(r"$\text{Electron DOS}$ / $\mathrm{eV^{-1}}$") 410 legend = ax.get_legend() 411 legend.set_loc("upper left") 412 legend.set_bbox_to_anchor((1.0, 1.0)) 413 legend.set_frame_on(False) 414 legend.set_title(None) 415 if config.edos_plot_prefix is not None: 416 fig.savefig(f"{config.edos_plot_prefix}_atm-spc.svg") 417 parprint( 418 "Projected (atomic specie) electron density of states plot" 419 + " saved..." 420 ) 421 422 edos_proj_vs_atm_ssh_da = edos_proj_da.groupby("atm_ssh").sum( 423 dim=["atm_spc", "atm_i", "harm_poly"] 424 ) 425 fig, ax = plt.subplots(tight_layout=True) 426 edos_proj_vs_atm_ssh_da.plot(x="energy", hue="atm_ssh", ax=ax) 427 ax.set_title("") 428 ax.set_xlabel(r"$\epsilon_{\text{KS}}$ / $\mathrm{eV}$") 429 ax.set_ylabel(r"$\text{Electron DOS}$ / $\mathrm{eV^{-1}}$") 430 legend = ax.get_legend() 431 legend.set_loc("upper left") 432 legend.set_bbox_to_anchor((1.0, 1.0)) 433 legend.set_frame_on(False) 434 legend.set_title(None) 435 if config.edos_plot_prefix is not None: 436 fig.savefig(f"{config.edos_plot_prefix}_atm-ssh.svg") 437 parprint( 438 "Projected (atomic subshell) electron density of states plot" 439 + " saved..." 440 ) 441 442 443def calc_edos_cli() -> None: 444 r"""Calculate electron density of states - CLI interface.""" 445 parser = ArgumentParser(description="Calculate electron density of states") 446 parser.add_argument( 447 "config", 448 nargs="?", 449 default="./config.yml", 450 help="configuration file", 451 ) 452 args = parser.parse_args() 453 with paropen(args.config, "r") as stream: 454 config = yaml.safe_load(stream) 455 config = Config(**config) 456 calc_edos(config)
Atomic subshells (index = l quantum number).
Atomic subshells to harmonic polynomials (index = GPAW m quantum number).
44class kptdenConvergenceSettings(BaseModel): 45 r"""$\mathbf{k}$-point density convergence settings.""" 46 47 value_min: confloat(gt=0.0, allow_inf_nan=False) = Field(frozen=True) 48 r"""Minimum value [$\mathrm{Å}$].""" 49 50 value_max: Optional[confloat(gt=0.0, allow_inf_nan=False)] = Field( 51 None, frozen=True 52 ) 53 r"""Maximum value [$\mathrm{Å}$].""" 54 55 value_spc: Optional[confloat(gt=0.0, allow_inf_nan=False)] = Field( 56 None, frozen=True 57 ) 58 r"""Value spacing [$\mathrm{Å}$].""" 59 60 threshold: Optional[confloat(gt=0.0, allow_inf_nan=False)] = Field( 61 None, frozen=True 62 ) 63 r"""Convergence threshold [$\mathrm{eV^{-1} \, Å^{-1}}$].""" 64 65 stability: Optional[conint(gt=0)] = Field(None, frozen=True) 66 r"""Convergence stability."""
$\mathbf{k}$-point density convergence settings.
Minimum value [$\mathrm{Å}$].
Maximum value [$\mathrm{Å}$].
Value spacing [$\mathrm{Å}$].
Convergence threshold [$\mathrm{eV^{-1} \, Å^{-1}}$].
Convergence stability.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Metadata about the fields defined on the model,
mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.
This replaces Model.__fields__ from Pydantic V1.
A dictionary of computed field names and their corresponding ComputedFieldInfo objects.
Inherited Members
- pydantic.main.BaseModel
- BaseModel
- model_extra
- model_fields_set
- model_construct
- model_copy
- model_dump
- model_dump_json
- model_json_schema
- model_parametrized_name
- model_post_init
- model_rebuild
- model_validate
- model_validate_json
- model_validate_strings
- dict
- json
- parse_obj
- parse_raw
- parse_file
- from_orm
- construct
- copy
- schema
- schema_json
- validate
- update_forward_refs
69class nbandsConvergenceSettings(BaseModel): 70 r"""Number of empty bands convergence settings.""" 71 72 value_min: conint(ge=0) = Field(frozen=True) 73 r"""Minimum value.""" 74 75 value_max: Optional[conint(ge=0)] = Field(None, frozen=True) 76 r"""Maximum value.""" 77 78 value_spc: Optional[conint(gt=0)] = Field(None, frozen=True) 79 r"""Value spacing.""" 80 81 threshold: Optional[confloat(gt=0.0, allow_inf_nan=False)] = Field( 82 None, frozen=True 83 ) 84 r"""Convergence threshold [$\mathrm{eV^{-1}}$].""" 85 86 stability: Optional[conint(gt=0)] = Field(None, frozen=True) 87 r"""Convergence stability."""
Number of empty bands convergence settings.
Maximum value.
Value spacing.
Convergence threshold [$\mathrm{eV^{-1}}$].
Convergence stability.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Metadata about the fields defined on the model,
mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.
This replaces Model.__fields__ from Pydantic V1.
A dictionary of computed field names and their corresponding ComputedFieldInfo objects.
Inherited Members
- pydantic.main.BaseModel
- BaseModel
- model_extra
- model_fields_set
- model_construct
- model_copy
- model_dump
- model_dump_json
- model_json_schema
- model_parametrized_name
- model_post_init
- model_rebuild
- model_validate
- model_validate_json
- model_validate_strings
- dict
- json
- parse_obj
- parse_raw
- parse_file
- from_orm
- construct
- copy
- schema
- schema_json
- validate
- update_forward_refs
90class Config(BaseModel): 91 r"""Configuration.""" 92 93 ground_path: constr(pattern=r".*\.gpw$") = Field(frozen=True) 94 r"""Path to electronic ground state.""" 95 96 kptden: Union[ 97 kptdenConvergenceSettings, 98 Mapping, 99 confloat(gt=0.0, allow_inf_nan=False), 100 ] = Field(10.0, frozen=True) 101 r"""$\mathbf{k}$-point density [$\mathrm{Å}$] convergence settings.""" 102 103 nbands_empty: Union[ 104 nbandsConvergenceSettings, 105 Mapping, 106 conint(ge=0), 107 ] = Field(0, frozen=True) 108 r"""Number of empty bands convergence settings.""" 109 110 strict: bool = Field(False, frozen=True) 111 r"""If True, max. slope is used for convergence; otherwise, avg. is used.""" 112 113 niter_max: conint(gt=0) = Field(1, frozen=True) 114 r"""Maximum number of iterations for convergence.""" 115 116 energy_min: confloat(lt=0.0, allow_inf_nan=False) = Field(-5.0, frozen=True) 117 r"""Electron energy minimum w.r.t. Fermi level [$\mathrm{eV}$].""" 118 119 energy_max: confloat(gt=0.0, allow_inf_nan=False) = Field(5.0, frozen=True) 120 r"""Electron energy maximum w.r.t. Fermi level [$\mathrm{eV}$].""" 121 122 energy_spc: confloat(gt=0.0, allow_inf_nan=False) = Field(0.01, frozen=True) 123 r"""Electron energy spacing [$\mathrm{eV}$].""" 124 125 energy_spc_prj: Optional[confloat(gt=0.0, allow_inf_nan=False)] = Field(None, frozen=True) 126 r"""Electron energy spacing for projected density of states.""" 127 128 projected: bool = Field(False, frozen=True) 129 r"""If True, also projected electron density of states is calculated.""" 130 131 gpaw_calc_prefix: Optional[str] = Field(None, frozen=True) 132 r"""Prefix to save GPAW calculator.""" 133 134 edos_save_prefix: Optional[str] = Field(None, frozen=True) 135 r"""Prefix to save electron density of states.""" 136 137 edos_plot_prefix: Optional[str] = Field(None, frozen=True) 138 r"""Prefix to save electron density of states plots.""" 139 140 conv_plot_root: Optional[str] = Field(None, frozen=True) 141 r"""Root to save convergence plots.""" 142 143 log_prefix: Optional[str] = Field(None, frozen=True) 144 r"""Prefix to save GPAW log.""" 145 146 cache_root: Optional[str] = Field(None, frozen=True) 147 r"""Root to cache electron densitysity of states.""" 148 149 parallel: Optional[Mapping] = Field(None, frozen=True) 150 r"""GPAW parallel options.""" 151 152 @field_validator("ground_path") 153 def validate_ground_path(cls, value: str) -> str: 154 r"""Validate `ground_path`. 155 156 Args: 157 value: Path to load electronic ground state. 158 159 Returns: 160 Path to load electronic ground state. 161 162 Raises: 163 FileNotFoundError: If `value` is not a path to an existing file. 164 """ 165 if not os.path.isfile(value): 166 raise FileNotFoundError 167 return value 168 169 @field_validator("kptden") 170 def parse_kptden_map(cls, value: Any) -> Any: 171 r"""Parse $\mathrm{k}$-point density convergence settings map.""" 172 if isinstance(value, Mapping): 173 value = kptdenConvergenceSettings(**value) 174 return value 175 176 @field_validator("nbands_empty") 177 def parse_nbands_empty_map(cls, value: Any) -> Any: 178 r"""Parse number of empty bands convergence settings map.""" 179 if isinstance(value, Mapping): 180 value = nbandsConvergenceSettings(**value) 181 return value 182 183 @field_validator( 184 "gpaw_calc_prefix", 185 "edos_save_prefix", 186 "edos_plot_prefix", 187 "conv_plot_root", 188 "log_prefix", 189 "cache_root", 190 ) 191 def create_tree(cls, value: Optional[str]) -> Optional[str]: 192 r"""Create tree for prefix or path. 193 194 Args: 195 value: Path or prefix of file to save. 196 197 Returns: 198 Path or prefix of file to save. 199 """ 200 if value: 201 tree = os.path.dirname(value) 202 if tree: 203 os.makedirs(tree, exist_ok=True) 204 return value
Configuration.
Path to electronic ground state.
$\mathbf{k}$-point density [$\mathrm{Å}$] convergence settings.
Number of empty bands convergence settings.
Maximum number of iterations for convergence.
Electron energy minimum w.r.t. Fermi level [$\mathrm{eV}$].
Electron energy maximum w.r.t. Fermi level [$\mathrm{eV}$].
Electron energy spacing [$\mathrm{eV}$].
Electron energy spacing for projected density of states.
152 @field_validator("ground_path") 153 def validate_ground_path(cls, value: str) -> str: 154 r"""Validate `ground_path`. 155 156 Args: 157 value: Path to load electronic ground state. 158 159 Returns: 160 Path to load electronic ground state. 161 162 Raises: 163 FileNotFoundError: If `value` is not a path to an existing file. 164 """ 165 if not os.path.isfile(value): 166 raise FileNotFoundError 167 return value
Validate ground_path.
Arguments:
- value: Path to load electronic ground state.
Returns:
Path to load electronic ground state.
Raises:
- FileNotFoundError: If
valueis not a path to an existing file.
169 @field_validator("kptden") 170 def parse_kptden_map(cls, value: Any) -> Any: 171 r"""Parse $\mathrm{k}$-point density convergence settings map.""" 172 if isinstance(value, Mapping): 173 value = kptdenConvergenceSettings(**value) 174 return value
Parse $\mathrm{k}$-point density convergence settings map.
176 @field_validator("nbands_empty") 177 def parse_nbands_empty_map(cls, value: Any) -> Any: 178 r"""Parse number of empty bands convergence settings map.""" 179 if isinstance(value, Mapping): 180 value = nbandsConvergenceSettings(**value) 181 return value
Parse number of empty bands convergence settings map.
183 @field_validator( 184 "gpaw_calc_prefix", 185 "edos_save_prefix", 186 "edos_plot_prefix", 187 "conv_plot_root", 188 "log_prefix", 189 "cache_root", 190 ) 191 def create_tree(cls, value: Optional[str]) -> Optional[str]: 192 r"""Create tree for prefix or path. 193 194 Args: 195 value: Path or prefix of file to save. 196 197 Returns: 198 Path or prefix of file to save. 199 """ 200 if value: 201 tree = os.path.dirname(value) 202 if tree: 203 os.makedirs(tree, exist_ok=True) 204 return value
Create tree for prefix or path.
Arguments:
- value: Path or prefix of file to save.
Returns:
Path or prefix of file to save.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Metadata about the fields defined on the model,
mapping of field names to [FieldInfo][pydantic.fields.FieldInfo] objects.
This replaces Model.__fields__ from Pydantic V1.
A dictionary of computed field names and their corresponding ComputedFieldInfo objects.
Inherited Members
- pydantic.main.BaseModel
- BaseModel
- model_extra
- model_fields_set
- model_construct
- model_copy
- model_dump
- model_dump_json
- model_json_schema
- model_parametrized_name
- model_post_init
- model_rebuild
- model_validate
- model_validate_json
- model_validate_strings
- dict
- json
- parse_obj
- parse_raw
- parse_file
- from_orm
- construct
- copy
- schema
- schema_json
- validate
- update_forward_refs
207def calc_edos(config: Config) -> None: 208 r"""Calculate electron densitysity of states. 209 210 Args: 211 config: Configuration. 212 """ 213 with redirect_to(config.log_prefix, mode="w"): 214 pass 215 parprint("GPAW log initialized...") 216 217 calc_cache = {} 218 calc_iter_i = 0 219 parprint("GPAW calculator cache initialized...") 220 221 ground = GPAW(config.ground_path) 222 parprint("Electronic ground state imported...") 223 224 nelectrons = ground.get_number_of_electrons() 225 nbands_occupied = ceil(0.5 * nelectrons) 226 parprint(f"Number of occupied bands: {nbands_occupied}...") 227 228 kptden_d = config.kptden 229 if isinstance(kptden_d, float): 230 kptden_d = {"value_min": kptden_d} 231 else: 232 kptden_d = kptden_d.dict() 233 kptden_d.update( 234 { 235 "direction": 1, 236 "criterion": f"slope-abs-{"max" if config.strict else "avg"}", 237 "symb": r"\lambda_{\mathbf{k}}", 238 "unit": r"Å", 239 } 240 ) 241 nbands_empty_d = config.nbands_empty 242 if isinstance(nbands_empty_d, int): 243 nbands_empty_d = {"value_min": nbands_empty_d} 244 else: 245 nbands_empty_d = nbands_empty_d.dict() 246 nbands_empty_d.update( 247 { 248 "direction": 1, 249 "criterion": f"slope-abs-{"max" if config.strict else "avg"}", 250 "symb": r"N^{*}", 251 "unit": r"1", 252 } 253 ) 254 params = {"kptden": kptden_d, "nbands_empty": nbands_empty_d} 255 parprint("Electron density of states convergence parameters set up...") 256 257 energy_grid = gen_regular_grid( 258 config.energy_min, config.energy_max, config.energy_spc 259 ) 260 parprint("Electron energy grid set up...") 261 262 @cache 263 def objective(kptden, nbands_empty): 264 nonlocal calc_iter_i 265 nbands_empty = int(nbands_empty) 266 nbands = nbands_occupied + nbands_empty 267 with redirect_to(config.log_prefix, mode="a"): 268 calc = ground.fixed_density( 269 kpts={"density": kptden, "gamma": True}, 270 nbands=nbands + nbands_empty, 271 convergence={"bands": nbands}, 272 parallel=config.parallel, 273 ) 274 calc_iter_i += 1 275 key = [("kptden", kptden), ("nbands_empty", nbands_empty)] 276 if config.cache_root is None: 277 calc_cache.update({tuple(sorted(key)): calc}) 278 else: 279 calc_path = os.path.join(config.cache_root, f"{calc_iter_i}.gpw") 280 calc.write(calc_path) 281 calc_cache.update({tuple(sorted(key)): calc_path}) 282 edos_calc = calc.dos() 283 edos = edos_calc.raw_dos(energy_grid, width=0.0) 284 return edos 285 286 parprint("Electron density of states objective function set up...") 287 288 convergence = MultivariateConvergence( 289 objective, 290 params, 291 crop=True, 292 req_sc=True, 293 niter_max=config.niter_max, 294 obj_symb=r"\text{Electron DOS}", 295 obj_unit=r"1/eV", 296 ) 297 parprint("Electron density of states convergence set up...") 298 299 parprint("Electron density of states convergence started...") 300 convergence.run() 301 302 if config.conv_plot_root is not None: 303 value_plots_map = convergence.plot("obj-value") 304 slope_plots_map = convergence.plot("obj-slope") 305 306 ndigits = len(str(config.niter_max)) 307 for qnty, plots_map in zip( 308 ["edos", "medos"], [value_plots_map, slope_plots_map] 309 ): 310 for param, plots_seq in plots_map.items(): 311 figdir = os.path.join( 312 config.conv_plot_root, f"{qnty}-vs-{param}" 313 ) 314 os.makedirs(figdir, exist_ok=True) 315 for i, plots in enumerate(plots_seq): 316 (plot,) = tuple(plots) 317 figpath = os.path.join(figdir, f"{i:0{ndigits}.0f}.svg") 318 fig = plot.get_figure() 319 fig.savefig(figpath) 320 plt.close(fig) 321 parprint("Converge plots saved...") 322 323 if convergence.converged is False: 324 parprint("Electron density of states convergence failed!") 325 else: 326 parprint("Electron density of states converged...") 327 328 params_opt = convergence.values_opt 329 key = tuple(sorted(params_opt.items())) 330 if key not in calc_cache: 331 objective(**params_opt) 332 333 if config.cache_root is None: 334 calc = calc_cache[key] 335 else: 336 calc = GPAW(calc_cache[key]) 337 if config.gpaw_calc_prefix is not None: 338 calc.write(f"{config.gpaw_calc_prefix}.gpw") 339 parprint("GPAW calculator saved...") 340 341 edos_calc = calc.dos() 342 edos = edos_calc.raw_dos(energy_grid, width=0.0) 343 parprint("Electron density of states calculated...") 344 345 edos_da = xr.DataArray(edos, coords=[energy_grid], dims=["energy"]) 346 edos_da.to_netcdf(f"{config.edos_save_prefix}_total.nc") 347 parprint("Electron density of states saved...") 348 349 fig, ax = plt.subplots(tight_layout=True) 350 ax.set_xlabel(r"$\epsilon_{\text{KS}}$ / $\mathrm{eV}$") 351 ax.set_ylabel(r"$\text{Electron DOS}$ / $\mathrm{eV^{-1}}$") 352 ax.plot(edos_da.energy.values, edos_da.values, color="k") 353 if config.edos_save_prefix is not None: 354 fig.savefig(f"{config.edos_plot_prefix}_total.svg") 355 parprint("Electron density of states plot saved...") 356 357 if config.projected is True: 358 if config.energy_spc_prj is not None: 359 energy_grid = gen_regular_grid( 360 config.energy_min, config.energy_max, config.energy_spc_prj 361 ) 362 parprint("Calculating projected electron density of states...") 363 edos_proj_da_seq = [] 364 atm_spc_seq = calc.atoms.get_chemical_symbols() 365 for atm_i, atm_spc in enumerate(atm_spc_seq): 366 for atm_ssh in atm_ssh_seq: 367 for harm_poly in sum(atm_ssh_to_harm_poly_seq.values(), []): 368 lqntnum = atm_ssh_seq.index(atm_ssh) 369 try: 370 mqntnum = atm_ssh_to_harm_poly_seq[atm_ssh].index( 371 harm_poly 372 ) 373 edos_proj = edos_calc.raw_pdos( 374 energy_grid, 375 a=atm_i, 376 l=lqntnum, 377 m=mqntnum, 378 width=0.0, 379 ) 380 except ValueError: 381 edos_proj = np.zeros(energy_grid.shape) 382 edos_proj_da = xr.DataArray( 383 edos_proj, coords=[energy_grid], dims=["energy"] 384 ) 385 edos_proj_da = ( 386 edos_proj_da.assign_coords(atm_spc=atm_spc) 387 .expand_dims("atm_spc") 388 .assign_coords(atm_i=atm_i) 389 .expand_dims("atm_i") 390 .assign_coords(atm_ssh=atm_ssh) 391 .expand_dims("atm_ssh") 392 .assign_coords(harm_poly=harm_poly) 393 .expand_dims("harm_poly") 394 ) 395 edos_proj_da_seq.append(edos_proj_da) 396 parprint("Projected electron density of states calculated...") 397 398 edos_proj_da = xr.combine_by_coords(edos_proj_da_seq) 399 if config.edos_save_prefix is not None: 400 edos_proj_da.to_netcdf(f"{config.edos_save_prefix}_projected.nc") 401 parprint("Projected electron density of states saved...") 402 403 edos_proj_vs_atm_spc_da = edos_proj_da.groupby("atm_spc").sum( 404 dim=["atm_i", "atm_ssh", "harm_poly"] 405 ) 406 fig, ax = plt.subplots(tight_layout=True) 407 edos_proj_vs_atm_spc_da.plot(x="energy", hue="atm_spc", ax=ax) 408 ax.set_title("") 409 ax.set_xlabel(r"$\epsilon_{\text{KS}}$ / $\mathrm{eV}$") 410 ax.set_ylabel(r"$\text{Electron DOS}$ / $\mathrm{eV^{-1}}$") 411 legend = ax.get_legend() 412 legend.set_loc("upper left") 413 legend.set_bbox_to_anchor((1.0, 1.0)) 414 legend.set_frame_on(False) 415 legend.set_title(None) 416 if config.edos_plot_prefix is not None: 417 fig.savefig(f"{config.edos_plot_prefix}_atm-spc.svg") 418 parprint( 419 "Projected (atomic specie) electron density of states plot" 420 + " saved..." 421 ) 422 423 edos_proj_vs_atm_ssh_da = edos_proj_da.groupby("atm_ssh").sum( 424 dim=["atm_spc", "atm_i", "harm_poly"] 425 ) 426 fig, ax = plt.subplots(tight_layout=True) 427 edos_proj_vs_atm_ssh_da.plot(x="energy", hue="atm_ssh", ax=ax) 428 ax.set_title("") 429 ax.set_xlabel(r"$\epsilon_{\text{KS}}$ / $\mathrm{eV}$") 430 ax.set_ylabel(r"$\text{Electron DOS}$ / $\mathrm{eV^{-1}}$") 431 legend = ax.get_legend() 432 legend.set_loc("upper left") 433 legend.set_bbox_to_anchor((1.0, 1.0)) 434 legend.set_frame_on(False) 435 legend.set_title(None) 436 if config.edos_plot_prefix is not None: 437 fig.savefig(f"{config.edos_plot_prefix}_atm-ssh.svg") 438 parprint( 439 "Projected (atomic subshell) electron density of states plot" 440 + " saved..." 441 )
Calculate electron densitysity of states.
Arguments:
- config: Configuration.
444def calc_edos_cli() -> None: 445 r"""Calculate electron density of states - CLI interface.""" 446 parser = ArgumentParser(description="Calculate electron density of states") 447 parser.add_argument( 448 "config", 449 nargs="?", 450 default="./config.yml", 451 help="configuration file", 452 ) 453 args = parser.parse_args() 454 with paropen(args.config, "r") as stream: 455 config = yaml.safe_load(stream) 456 config = Config(**config) 457 calc_edos(config)
Calculate electron density of states - CLI interface.