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)
atm_ssh_seq: list[str, str, str, str] = ['s', 'p', 'd', 'f']

Atomic subshells (index = l quantum number).

atm_ssh_to_harm_poly_seq: dict[str, list[str, ...]] = {'s': [''], 'p': ['y', 'z', 'x'], 'd': ['xy', 'yz', 'z2', 'xz', 'x2-y2'], 'f': ['y3', 'xyz', 'yz2', 'z3', 'xz2', 'x2z-y2z', 'x3']}

Atomic subshells to harmonic polynomials (index = GPAW m quantum number).

class kptdenConvergenceSettings(pydantic.main.BaseModel):
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.

value_min: Annotated[float, None, Interval(gt=0.0, ge=None, lt=None, le=None), None, AllowInfNan(allow_inf_nan=False)]

Minimum value [$\mathrm{Å}$].

value_max: Optional[Annotated[float, None, Interval(gt=0.0, ge=None, lt=None, le=None), None, AllowInfNan(allow_inf_nan=False)]]

Maximum value [$\mathrm{Å}$].

value_spc: Optional[Annotated[float, None, Interval(gt=0.0, ge=None, lt=None, le=None), None, AllowInfNan(allow_inf_nan=False)]]

Value spacing [$\mathrm{Å}$].

threshold: Optional[Annotated[float, None, Interval(gt=0.0, ge=None, lt=None, le=None), None, AllowInfNan(allow_inf_nan=False)]]

Convergence threshold [$\mathrm{eV^{-1} \, Å^{-1}}$].

stability: Optional[Annotated[int, None, Interval(gt=0, ge=None, lt=None, le=None), None]]

Convergence stability.

model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, pydantic.fields.FieldInfo]] = {'value_min': FieldInfo(annotation=float, required=True, frozen=True, metadata=[None, Interval(gt=0.0, ge=None, lt=None, le=None), None, AllowInfNan(allow_inf_nan=False)]), 'value_max': FieldInfo(annotation=Union[Annotated[float, NoneType, Interval, NoneType, AllowInfNan(allow_inf_nan=False)], NoneType], required=False, default=None, frozen=True), 'value_spc': FieldInfo(annotation=Union[Annotated[float, NoneType, Interval, NoneType, AllowInfNan(allow_inf_nan=False)], NoneType], required=False, default=None, frozen=True), 'threshold': FieldInfo(annotation=Union[Annotated[float, NoneType, Interval, NoneType, AllowInfNan(allow_inf_nan=False)], NoneType], required=False, default=None, frozen=True), 'stability': FieldInfo(annotation=Union[Annotated[int, NoneType, Interval, NoneType], NoneType], required=False, default=None, frozen=True)}

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.

model_computed_fields: ClassVar[Dict[str, pydantic.fields.ComputedFieldInfo]] = {}

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
class nbandsConvergenceSettings(pydantic.main.BaseModel):
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.

value_min: Annotated[int, None, Interval(gt=None, ge=0, lt=None, le=None), None]

Minimum value.

value_max: Optional[Annotated[int, None, Interval(gt=None, ge=0, lt=None, le=None), None]]

Maximum value.

value_spc: Optional[Annotated[int, None, Interval(gt=0, ge=None, lt=None, le=None), None]]

Value spacing.

threshold: Optional[Annotated[float, None, Interval(gt=0.0, ge=None, lt=None, le=None), None, AllowInfNan(allow_inf_nan=False)]]

Convergence threshold [$\mathrm{eV^{-1}}$].

stability: Optional[Annotated[int, None, Interval(gt=0, ge=None, lt=None, le=None), None]]

Convergence stability.

model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, pydantic.fields.FieldInfo]] = {'value_min': FieldInfo(annotation=int, required=True, frozen=True, metadata=[None, Interval(gt=None, ge=0, lt=None, le=None), None]), 'value_max': FieldInfo(annotation=Union[Annotated[int, NoneType, Interval, NoneType], NoneType], required=False, default=None, frozen=True), 'value_spc': FieldInfo(annotation=Union[Annotated[int, NoneType, Interval, NoneType], NoneType], required=False, default=None, frozen=True), 'threshold': FieldInfo(annotation=Union[Annotated[float, NoneType, Interval, NoneType, AllowInfNan(allow_inf_nan=False)], NoneType], required=False, default=None, frozen=True), 'stability': FieldInfo(annotation=Union[Annotated[int, NoneType, Interval, NoneType], NoneType], required=False, default=None, frozen=True)}

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.

model_computed_fields: ClassVar[Dict[str, pydantic.fields.ComputedFieldInfo]] = {}

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
class Config(pydantic.main.BaseModel):
 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.

ground_path: Annotated[str, StringConstraints(strip_whitespace=None, to_upper=None, to_lower=None, strict=None, min_length=None, max_length=None, pattern='.*\\.gpw$')]

Path to electronic ground state.

kptden: Union[kptdenConvergenceSettings, collections.abc.Mapping, Annotated[float, None, Interval(gt=0.0, ge=None, lt=None, le=None), None, AllowInfNan(allow_inf_nan=False)]]

$\mathbf{k}$-point density [$\mathrm{Å}$] convergence settings.

nbands_empty: Union[nbandsConvergenceSettings, collections.abc.Mapping, Annotated[int, None, Interval(gt=None, ge=0, lt=None, le=None), None]]

Number of empty bands convergence settings.

strict: bool

If True, max. slope is used for convergence; otherwise, avg. is used.

niter_max: Annotated[int, None, Interval(gt=0, ge=None, lt=None, le=None), None]

Maximum number of iterations for convergence.

energy_min: Annotated[float, None, Interval(gt=None, ge=None, lt=0.0, le=None), None, AllowInfNan(allow_inf_nan=False)]

Electron energy minimum w.r.t. Fermi level [$\mathrm{eV}$].

energy_max: Annotated[float, None, Interval(gt=0.0, ge=None, lt=None, le=None), None, AllowInfNan(allow_inf_nan=False)]

Electron energy maximum w.r.t. Fermi level [$\mathrm{eV}$].

energy_spc: Annotated[float, None, Interval(gt=0.0, ge=None, lt=None, le=None), None, AllowInfNan(allow_inf_nan=False)]

Electron energy spacing [$\mathrm{eV}$].

energy_spc_prj: Optional[Annotated[float, None, Interval(gt=0.0, ge=None, lt=None, le=None), None, AllowInfNan(allow_inf_nan=False)]]

Electron energy spacing for projected density of states.

projected: bool

If True, also projected electron density of states is calculated.

gpaw_calc_prefix: Optional[str]

Prefix to save GPAW calculator.

edos_save_prefix: Optional[str]

Prefix to save electron density of states.

edos_plot_prefix: Optional[str]

Prefix to save electron density of states plots.

conv_plot_root: Optional[str]

Root to save convergence plots.

log_prefix: Optional[str]

Prefix to save GPAW log.

cache_root: Optional[str]

Root to cache electron densitysity of states.

parallel: Optional[collections.abc.Mapping]

GPAW parallel options.

@field_validator('ground_path')
def validate_ground_path(cls, value: str) -> str:
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 value is not a path to an existing file.
@field_validator('kptden')
def parse_kptden_map(cls, value: Any) -> Any:
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.

@field_validator('nbands_empty')
def parse_nbands_empty_map(cls, value: Any) -> Any:
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.

@field_validator('gpaw_calc_prefix', 'edos_save_prefix', 'edos_plot_prefix', 'conv_plot_root', 'log_prefix', 'cache_root')
def create_tree(cls, value: Optional[str]) -> Optional[str]:
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.

model_config: ClassVar[pydantic.config.ConfigDict] = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, pydantic.fields.FieldInfo]] = {'ground_path': FieldInfo(annotation=str, required=True, frozen=True, metadata=[StringConstraints(strip_whitespace=None, to_upper=None, to_lower=None, strict=None, min_length=None, max_length=None, pattern='.*\\.gpw$')]), 'kptden': FieldInfo(annotation=Union[kptdenConvergenceSettings, Mapping, Annotated[float, NoneType, Interval, NoneType, AllowInfNan(allow_inf_nan=False)]], required=False, default=10.0, frozen=True), 'nbands_empty': FieldInfo(annotation=Union[nbandsConvergenceSettings, Mapping, Annotated[int, NoneType, Interval, NoneType]], required=False, default=0, frozen=True), 'strict': FieldInfo(annotation=bool, required=False, default=False, frozen=True), 'niter_max': FieldInfo(annotation=int, required=False, default=1, frozen=True, metadata=[None, Interval(gt=0, ge=None, lt=None, le=None), None]), 'energy_min': FieldInfo(annotation=float, required=False, default=-5.0, frozen=True, metadata=[None, Interval(gt=None, ge=None, lt=0.0, le=None), None, AllowInfNan(allow_inf_nan=False)]), 'energy_max': FieldInfo(annotation=float, required=False, default=5.0, frozen=True, metadata=[None, Interval(gt=0.0, ge=None, lt=None, le=None), None, AllowInfNan(allow_inf_nan=False)]), 'energy_spc': FieldInfo(annotation=float, required=False, default=0.01, frozen=True, metadata=[None, Interval(gt=0.0, ge=None, lt=None, le=None), None, AllowInfNan(allow_inf_nan=False)]), 'energy_spc_prj': FieldInfo(annotation=Union[Annotated[float, NoneType, Interval, NoneType, AllowInfNan(allow_inf_nan=False)], NoneType], required=False, default=None, frozen=True), 'projected': FieldInfo(annotation=bool, required=False, default=False, frozen=True), 'gpaw_calc_prefix': FieldInfo(annotation=Union[str, NoneType], required=False, default=None, frozen=True), 'edos_save_prefix': FieldInfo(annotation=Union[str, NoneType], required=False, default=None, frozen=True), 'edos_plot_prefix': FieldInfo(annotation=Union[str, NoneType], required=False, default=None, frozen=True), 'conv_plot_root': FieldInfo(annotation=Union[str, NoneType], required=False, default=None, frozen=True), 'log_prefix': FieldInfo(annotation=Union[str, NoneType], required=False, default=None, frozen=True), 'cache_root': FieldInfo(annotation=Union[str, NoneType], required=False, default=None, frozen=True), 'parallel': FieldInfo(annotation=Union[Mapping, NoneType], required=False, default=None, frozen=True)}

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.

model_computed_fields: ClassVar[Dict[str, pydantic.fields.ComputedFieldInfo]] = {}

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
def calc_edos(config: Config) -> None:
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.
def calc_edos_cli() -> None:
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.