"""Flow for electrode analysis."""
from __future__ import annotations
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from jobflow import Flow, Maker
from pymatgen.analysis.structure_matcher import ElementComparator, StructureMatcher
from atomate2.common.jobs.electrode import (
RelaxJobSummary,
get_computed_entries,
get_insertion_electrode_doc,
get_stable_inserted_results,
get_structure_group_doc,
)
if TYPE_CHECKING:
from pathlib import Path
from pymatgen.alchemy import ElementLike
from pymatgen.core.structure import Structure
from pymatgen.entries.computed_entries import ComputedEntry
from pymatgen.io.vasp.outputs import VolumetricData
logger = logging.getLogger(__name__)
__author__ = "Jimmy Shen"
__email__ = "jmmshn@gmail.com"
[docs]
@dataclass
class ElectrodeInsertionMaker(Maker, ABC):
"""Attempt ion insertion into a structure.
The basic unit for cation insertion is:
[get_stable_inserted_structure]:
(static) -> (chgcar analysis) ->
N x (relax) -> (return best structure)
The workflow is:
[relax structure]
[get_stable_inserted_structure]
[get_stable_inserted_structure]
[get_stable_inserted_structure]
... until the insertion is no longer topotactic.
This workflow requires the users to provide the following functions:
self.get_charge_density(task_doc: TaskDoc):
Get the charge density of a TaskDoc output from a calculation.
self.update_static_maker():
Ensure that the static maker will store the desired data.
If you use this workflow please cite the following paper:
Shen, J.-X., Horton, M., & Persson, K. A. (2020).
A charge-density-based general cation insertion algorithm for
generating new Li-ion cathode materials.
npj Computational Materials, 6(161), 1—7.
doi: 10.1038/s41524-020-00422-3
Attributes
----------
name: str
The name of the flow created by this maker.
relax_maker: RelaxMaker
A maker to perform relaxation calculations.
bulk_relax_maker: Maker
A separate maker to perform the first bulk relaxation calculation.
If None, the relax_maker will be used.
static_maker: Maker
A maker to perform static calculations.
structure_matcher: StructureMatcher
The structure matcher to use to determine if additional insertion is needed.
"""
relax_maker: Maker
static_maker: Maker
bulk_relax_maker: Maker | None = None
name: str = "ion insertion"
structure_matcher: StructureMatcher = field(
default_factory=lambda: StructureMatcher(
comparator=ElementComparator(),
)
)
def __post_init__(self) -> None:
"""Ensure that the static maker will store the desired data."""
self.update_static_maker()
[docs]
def make(
self,
structure: Structure,
inserted_element: ElementLike,
n_steps: int | None,
insertions_per_step: int = 4,
working_ion_entry: ComputedEntry | None = None,
) -> Flow:
"""Make the flow.
Parameters
----------
structure:
Structure to insert ion into.
inserted_species:
Species to insert.
n_steps: int
The maximum number of sequential insertion steps to attempt.
insertions_per_step: int
The maximum number of ion insertion sites to attempt.
Returns
-------
Flow for ion insertion.
"""
# First relax the structure
if self.bulk_relax_maker:
relax = self.bulk_relax_maker.make(structure)
else:
relax = self.relax_maker.make(structure)
# add ignored_species to the structure matcher
sm = _add_ignored_species(self.structure_matcher, inserted_element)
# Get the inserted structure
new_entries_job = get_stable_inserted_results(
structure=relax.output.structure,
inserted_element=inserted_element,
structure_matcher=sm,
static_maker=self.static_maker,
relax_maker=self.relax_maker,
get_charge_density=self.get_charge_density,
n_steps=n_steps,
insertions_per_step=insertions_per_step,
)
relaxed_summary = RelaxJobSummary(
structure=relax.output.structure,
entry=relax.output.entry,
dir_name=relax.output.dir_name,
uuid=relax.output.uuid,
)
get_entries_job = get_computed_entries(new_entries_job.output, relaxed_summary)
structure_group_job = get_structure_group_doc(
get_entries_job.output, ignored_species=str(inserted_element)
)
jobs = [relax, new_entries_job, get_entries_job, structure_group_job]
output = structure_group_job.output
if working_ion_entry:
insertion_electrode_job = get_insertion_electrode_doc(
get_entries_job.output, working_ion_entry
)
jobs.append(insertion_electrode_job)
output = insertion_electrode_job.output
return Flow(jobs=jobs, output=output)
[docs]
@abstractmethod
def get_charge_density(self, prev_dir: Path | str) -> VolumetricData:
"""Get the charge density of a structure.
Parameters
----------
prev_dir:
The previous directory where the static calculation was performed.
Returns
-------
The charge density.
"""
[docs]
@abstractmethod
def update_static_maker(self) -> None:
"""Ensure that the static maker will store the desired data."""
def _add_ignored_species(
structure_matcher: StructureMatcher, species: ElementLike
) -> StructureMatcher:
"""Add an ignored species to a structure matcher."""
sm_dict = structure_matcher.as_dict()
ignored_species = set(sm_dict.get("ignored_species", set()))
ignored_species.add(str(species))
sm_dict["ignored_species"] = list(ignored_species)
return StructureMatcher.from_dict(sm_dict)