madman.helpers.ase.workflows.ebands

Electronic band structure.

  1r"""Electronic band structure."""
  2
  3import os
  4from argparse import ArgumentParser
  5from collections.abc import Mapping
  6from math import ceil
  7from typing import Optional
  8
  9import seaborn as sns
 10import yaml
 11from ase.parallel import paropen, parprint
 12from gpaw.calculator import GPAW
 13from matplotlib import pyplot as plt
 14from pydantic import BaseModel, Field, confloat, conint, constr, field_validator
 15
 16from madman.utilities import redirect_to
 17
 18
 19sns.set_theme(
 20    context="talk",
 21    style="white",
 22    rc={"figure.titlesize": "medium", "axes.formatter.useoffset": False},
 23)
 24
 25
 26class Config(BaseModel):
 27    r"""Configuration."""
 28
 29    ground_path: constr(pattern=r".*\.gpw$") = Field(frozen=True)
 30    r"""Path to electronic ground state."""
 31
 32    kptden: confloat(gt=0.0, allow_inf_nan=False) = Field(10.0, frozen=True)
 33    r"""$\mathbf{k}$-point density [$\mathrm{Å}$]."""
 34
 35    nbands_empty: conint(ge=0) = Field(0, frozen=True)
 36    r"""Number of empty bands."""
 37
 38    energy_min: confloat(lt=0.0, allow_inf_nan=False) = Field(-5.0, frozen=True)
 39    r"""Electron energy minimum w.r.t. Fermi level [$\mathrm{eV}$]."""
 40
 41    energy_max: confloat(gt=0.0, allow_inf_nan=False) = Field(5.0, frozen=True)
 42    r"""Electron energy maximum w.r.t. Fermi level [$\mathrm{eV}$]."""
 43
 44    ebands_save_prefix: Optional[str] = Field(None, frozen=True)
 45    r"""Prefix to save electronic band structure."""
 46
 47    ebands_plot_prefix: Optional[str] = Field(None, frozen=True)
 48    r"""Prefix to save electronic band structure plot."""
 49
 50    log_prefix: Optional[str] = Field(None, frozen=True)
 51    r"""Prefix to save GPAW log."""
 52
 53    parallel: Optional[Mapping] = Field(None, frozen=True)
 54    r"""GPAW parallel options."""
 55
 56    @field_validator("ground_path")
 57    def validate_ground_path(cls, value: str) -> str:
 58        r"""Validate `ground_path`.
 59
 60        Args:
 61            value: Path to load electronic ground state.
 62
 63        Returns:
 64            Path to load electronic ground state.
 65
 66        Raises:
 67            FileNotFoundError: If `value` is not a path to an existing file.
 68        """
 69        if not os.path.isfile(value):
 70            raise FileNotFoundError
 71        return value
 72
 73    @field_validator("ebands_save_prefix", "ebands_plot_prefix", "log_prefix")
 74    def create_tree(cls, value: Optional[str]) -> Optional[str]:
 75        r"""Create tree for prefix or path.
 76
 77        Args:
 78            value: Path or prefix of file to save.
 79
 80        Returns:
 81            Path or prefix of file to save.
 82        """
 83        if value:
 84            tree = os.path.dirname(value)
 85            if tree:
 86                os.makedirs(tree, exist_ok=True)
 87        return value
 88
 89
 90def calc_ebands(config: Config) -> None:
 91    r"""Calculate electronic band structure.
 92
 93    Args:
 94        config: Configuration.
 95    """
 96    ground = GPAW(config.ground_path)
 97    parprint("Electronic ground state imported...")
 98
 99    kpath = ground.atoms.cell.bandpath(density=config.kptden)
100    nelectrons = ground.get_number_of_electrons()
101    nbands_occupied = ceil(0.5 * nelectrons)
102    nbands = nbands_occupied + config.nbands_empty
103
104    with redirect_to(config.log_prefix, mode="w"):
105        ebands_calc = ground.fixed_density(
106            kpts=kpath.kpts,
107            symmetry="off",
108            nbands=nbands_occupied + 2 * config.nbands_empty,
109            convergence={"bands": nbands},
110            parallel=config.parallel,
111        )
112    ebands = ebands_calc.band_structure().subtract_reference()
113    parprint("Electronic band structure calculated...")
114
115    if config.ebands_save_prefix:
116        ebands.write(f"{config.ebands_save_prefix}.json")
117        parprint("Electronic band structure saved...")
118
119    fig, ax = plt.subplots(tight_layout=True)
120    ebands.plot(
121        ax=ax,
122        ylabel=r"$\epsilon_{\text{KS}}$ / $\mathrm{eV}$",
123        emin=config.energy_min,
124        emax=config.energy_max,
125        colors="k",
126    )
127    ax.set_xticklabels(
128        [
129            xlabel.get_text().replace(",", "|")
130            for xlabel in ax.get_xmajorticklabels()
131        ]
132    )
133
134    if config.ebands_plot_prefix:
135        fig.savefig(f"{config.ebands_plot_prefix}.svg")
136        parprint("Electronic band structure plot saved...")
137
138
139def calc_ebands_cli() -> None:
140    r"""Calculate electronic band structure - CLI interface."""
141    parser = ArgumentParser(description="Calculate electronic band structure")
142    parser.add_argument(
143        "config",
144        nargs="?",
145        default="./config.yml",
146        help="configuration file",
147    )
148    args = parser.parse_args()
149    with paropen(args.config, "r") as stream:
150        config = yaml.safe_load(stream)
151    config = Config(**config)
152    calc_ebands(config)
class Config(pydantic.main.BaseModel):
27class Config(BaseModel):
28    r"""Configuration."""
29
30    ground_path: constr(pattern=r".*\.gpw$") = Field(frozen=True)
31    r"""Path to electronic ground state."""
32
33    kptden: confloat(gt=0.0, allow_inf_nan=False) = Field(10.0, frozen=True)
34    r"""$\mathbf{k}$-point density [$\mathrm{Å}$]."""
35
36    nbands_empty: conint(ge=0) = Field(0, frozen=True)
37    r"""Number of empty bands."""
38
39    energy_min: confloat(lt=0.0, allow_inf_nan=False) = Field(-5.0, frozen=True)
40    r"""Electron energy minimum w.r.t. Fermi level [$\mathrm{eV}$]."""
41
42    energy_max: confloat(gt=0.0, allow_inf_nan=False) = Field(5.0, frozen=True)
43    r"""Electron energy maximum w.r.t. Fermi level [$\mathrm{eV}$]."""
44
45    ebands_save_prefix: Optional[str] = Field(None, frozen=True)
46    r"""Prefix to save electronic band structure."""
47
48    ebands_plot_prefix: Optional[str] = Field(None, frozen=True)
49    r"""Prefix to save electronic band structure plot."""
50
51    log_prefix: Optional[str] = Field(None, frozen=True)
52    r"""Prefix to save GPAW log."""
53
54    parallel: Optional[Mapping] = Field(None, frozen=True)
55    r"""GPAW parallel options."""
56
57    @field_validator("ground_path")
58    def validate_ground_path(cls, value: str) -> str:
59        r"""Validate `ground_path`.
60
61        Args:
62            value: Path to load electronic ground state.
63
64        Returns:
65            Path to load electronic ground state.
66
67        Raises:
68            FileNotFoundError: If `value` is not a path to an existing file.
69        """
70        if not os.path.isfile(value):
71            raise FileNotFoundError
72        return value
73
74    @field_validator("ebands_save_prefix", "ebands_plot_prefix", "log_prefix")
75    def create_tree(cls, value: Optional[str]) -> Optional[str]:
76        r"""Create tree for prefix or path.
77
78        Args:
79            value: Path or prefix of file to save.
80
81        Returns:
82            Path or prefix of file to save.
83        """
84        if value:
85            tree = os.path.dirname(value)
86            if tree:
87                os.makedirs(tree, exist_ok=True)
88        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: Annotated[float, None, Interval(gt=0.0, ge=None, lt=None, le=None), None, AllowInfNan(allow_inf_nan=False)]

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

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

Number of empty bands.

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}$].

ebands_save_prefix: Optional[str]

Prefix to save electronic band structure.

ebands_plot_prefix: Optional[str]

Prefix to save electronic band structure plot.

log_prefix: Optional[str]

Prefix to save GPAW log.

parallel: Optional[collections.abc.Mapping]

GPAW parallel options.

@field_validator('ground_path')
def validate_ground_path(cls, value: str) -> str:
57    @field_validator("ground_path")
58    def validate_ground_path(cls, value: str) -> str:
59        r"""Validate `ground_path`.
60
61        Args:
62            value: Path to load electronic ground state.
63
64        Returns:
65            Path to load electronic ground state.
66
67        Raises:
68            FileNotFoundError: If `value` is not a path to an existing file.
69        """
70        if not os.path.isfile(value):
71            raise FileNotFoundError
72        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('ebands_save_prefix', 'ebands_plot_prefix', 'log_prefix')
def create_tree(cls, value: Optional[str]) -> Optional[str]:
74    @field_validator("ebands_save_prefix", "ebands_plot_prefix", "log_prefix")
75    def create_tree(cls, value: Optional[str]) -> Optional[str]:
76        r"""Create tree for prefix or path.
77
78        Args:
79            value: Path or prefix of file to save.
80
81        Returns:
82            Path or prefix of file to save.
83        """
84        if value:
85            tree = os.path.dirname(value)
86            if tree:
87                os.makedirs(tree, exist_ok=True)
88        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=float, required=False, default=10.0, frozen=True, metadata=[None, Interval(gt=0.0, ge=None, lt=None, le=None), None, AllowInfNan(allow_inf_nan=False)]), 'nbands_empty': FieldInfo(annotation=int, required=False, default=0, frozen=True, metadata=[None, Interval(gt=None, ge=0, 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)]), 'ebands_save_prefix': FieldInfo(annotation=Union[str, NoneType], required=False, default=None, frozen=True), 'ebands_plot_prefix': FieldInfo(annotation=Union[str, NoneType], required=False, default=None, frozen=True), 'log_prefix': 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_ebands(config: Config) -> None:
 91def calc_ebands(config: Config) -> None:
 92    r"""Calculate electronic band structure.
 93
 94    Args:
 95        config: Configuration.
 96    """
 97    ground = GPAW(config.ground_path)
 98    parprint("Electronic ground state imported...")
 99
100    kpath = ground.atoms.cell.bandpath(density=config.kptden)
101    nelectrons = ground.get_number_of_electrons()
102    nbands_occupied = ceil(0.5 * nelectrons)
103    nbands = nbands_occupied + config.nbands_empty
104
105    with redirect_to(config.log_prefix, mode="w"):
106        ebands_calc = ground.fixed_density(
107            kpts=kpath.kpts,
108            symmetry="off",
109            nbands=nbands_occupied + 2 * config.nbands_empty,
110            convergence={"bands": nbands},
111            parallel=config.parallel,
112        )
113    ebands = ebands_calc.band_structure().subtract_reference()
114    parprint("Electronic band structure calculated...")
115
116    if config.ebands_save_prefix:
117        ebands.write(f"{config.ebands_save_prefix}.json")
118        parprint("Electronic band structure saved...")
119
120    fig, ax = plt.subplots(tight_layout=True)
121    ebands.plot(
122        ax=ax,
123        ylabel=r"$\epsilon_{\text{KS}}$ / $\mathrm{eV}$",
124        emin=config.energy_min,
125        emax=config.energy_max,
126        colors="k",
127    )
128    ax.set_xticklabels(
129        [
130            xlabel.get_text().replace(",", "|")
131            for xlabel in ax.get_xmajorticklabels()
132        ]
133    )
134
135    if config.ebands_plot_prefix:
136        fig.savefig(f"{config.ebands_plot_prefix}.svg")
137        parprint("Electronic band structure plot saved...")

Calculate electronic band structure.

Arguments:
  • config: Configuration.
def calc_ebands_cli() -> None:
140def calc_ebands_cli() -> None:
141    r"""Calculate electronic band structure - CLI interface."""
142    parser = ArgumentParser(description="Calculate electronic band structure")
143    parser.add_argument(
144        "config",
145        nargs="?",
146        default="./config.yml",
147        help="configuration file",
148    )
149    args = parser.parse_args()
150    with paropen(args.config, "r") as stream:
151        config = yaml.safe_load(stream)
152    config = Config(**config)
153    calc_ebands(config)

Calculate electronic band structure - CLI interface.