diff --git a/src/py/mat3ra/made/tools/build/defect/__init__.py b/src/py/mat3ra/made/tools/build/defect/__init__.py index db3359c4..4d2f5759 100644 --- a/src/py/mat3ra/made/tools/build/defect/__init__.py +++ b/src/py/mat3ra/made/tools/build/defect/__init__.py @@ -30,9 +30,11 @@ def create_defect( Returns: The material with the defect added. """ - BuilderClass = DefectBuilderFactory.get_class_by_name(configuration.defect_type) + defect_builder_key = configuration.defect_type.lower() + if configuration.placement_method is not None: + defect_builder_key = f"{defect_builder_key}:{configuration.placement_method.lower()}" + BuilderClass = DefectBuilderFactory.get_class_by_name(defect_builder_key) builder = BuilderClass(builder_parameters) - return builder.get_material(configuration) if builder else configuration.crystal diff --git a/src/py/mat3ra/made/tools/build/defect/builders.py b/src/py/mat3ra/made/tools/build/defect/builders.py index d9a6524f..f022d6e2 100644 --- a/src/py/mat3ra/made/tools/build/defect/builders.py +++ b/src/py/mat3ra/made/tools/build/defect/builders.py @@ -6,12 +6,14 @@ from mat3ra.made.utils import get_center_of_coordinates + from ...third_party import ( PymatgenStructure, PymatgenPeriodicSite, PymatgenVacancy, PymatgenSubstitution, PymatgenInterstitial, + PymatgenVoronoiInterstitialGenerator, ) from ...modify import ( @@ -31,7 +33,11 @@ get_local_extremum_atom_index, ) from ...analyze.coordination import get_voronoi_nearest_neighbors_atom_indices -from ...utils import transform_coordinate_to_supercell, coordinate as CoordinateCondition +from ...utils import ( + transform_coordinate_to_supercell, + coordinate as CoordinateCondition, + get_distance_between_coordinates, +) from ..utils import merge_materials from ..slab import SlabConfiguration, create_slab, Termination from ..supercell import create_supercell @@ -62,7 +68,7 @@ class PointDefectBuilder(ConvertGeneratedItemsPymatgenStructureMixin, DefectBuil _BuildParametersType = PointDefectBuilderParameters _DefaultBuildParameters = PointDefectBuilderParameters() - _GeneratedItemType: PymatgenStructure = PymatgenStructure + _GeneratedItemType: type(PymatgenStructure) = PymatgenStructure # type: ignore _ConfigurationType = PointDefectConfiguration _generator: Callable @@ -106,6 +112,43 @@ class InterstitialPointDefectBuilder(PointDefectBuilder): _generator: PymatgenInterstitial = PymatgenInterstitial +class VoronoiInterstitialPointDefectBuilderParameters(BaseModel): + # According to pymatgen: + # https://github.com/materialsproject/pymatgen-analysis-defects/blob/e2cb285de8be07b38912ae1782285ef1f463a9a9/pymatgen/analysis/defects/generators.py#L343 + clustering_tol: float = 0.5 # Clustering tolerance for merging interstitial sites + min_dist: float = 0.9 # Minimum distance between interstitial and nearest atom + ltol: float = 0.2 # Tolerance for lattice matching. + stol: float = 0.3 # Tolerance for structure matching. + angle_tol: float = 5 # Angle tolerance for structure matching. + + +class VoronoiInterstitialPointDefectBuilder(PointDefectBuilder): + _BuildParametersType: type( # type: ignore + VoronoiInterstitialPointDefectBuilderParameters + ) = VoronoiInterstitialPointDefectBuilderParameters # type: ignore + _DefaultBuildParameters = VoronoiInterstitialPointDefectBuilderParameters() # type: ignore + + def _generate( + self, configuration: BaseBuilder._ConfigurationType + ) -> List[type(PointDefectBuilder._GeneratedItemType)]: # type: ignore + pymatgen_structure = to_pymatgen(configuration.crystal) + voronoi_gen = PymatgenVoronoiInterstitialGenerator( + **self.build_parameters.dict(), + ) + interstitials = list( + voronoi_gen.generate(structure=pymatgen_structure, insert_species=[configuration.chemical_element]) + ) + approximate_coordinate = configuration.coordinate + closest_interstitial = min( + interstitials, + key=lambda interstitial: get_distance_between_coordinates( + interstitial.site.frac_coords, approximate_coordinate + ), + ) + pymatgen_structure.append(closest_interstitial.site.species, closest_interstitial.site.frac_coords) + return [pymatgen_structure] + + class SlabDefectBuilderParameters(BaseModel): auto_add_vacuum: bool = True vacuum_thickness: float = 5.0 diff --git a/src/py/mat3ra/made/tools/build/defect/configuration.py b/src/py/mat3ra/made/tools/build/defect/configuration.py index b4a29773..15eb8ac0 100644 --- a/src/py/mat3ra/made/tools/build/defect/configuration.py +++ b/src/py/mat3ra/made/tools/build/defect/configuration.py @@ -58,6 +58,7 @@ class PointDefectConfiguration(BaseDefectConfiguration, InMemoryEntity): coordinate: List[float] = [0, 0, 0] chemical_element: Optional[str] = None use_cartesian_coordinates: bool = False + placement_method: Optional[AtomPlacementMethodEnum] = None @classmethod def from_site_id( @@ -107,6 +108,8 @@ def _json(self): "defect_type": self.defect_type.name, "coordinate": self.coordinate, "chemical_element": self.chemical_element, + "use_cartesian_coordinates": self.use_cartesian_coordinates, + "placement_method": self.placement_method.name if self.placement_method else None, } diff --git a/src/py/mat3ra/made/tools/build/defect/enums.py b/src/py/mat3ra/made/tools/build/defect/enums.py index bdc22279..284cb53b 100644 --- a/src/py/mat3ra/made/tools/build/defect/enums.py +++ b/src/py/mat3ra/made/tools/build/defect/enums.py @@ -26,6 +26,8 @@ class AtomPlacementMethodEnum(str, Enum): EQUIDISTANT = "equidistant" # Places the atom at the existing or extrapolated crystal site closest to the given coordinate. CRYSTAL_SITE = "crystal_site" + # Places the atom at Voronoi site closest to the given coordinate. + VORONOI_SITE = "voronoi_site" class CoordinatesShapeEnum(str, Enum): diff --git a/src/py/mat3ra/made/tools/build/defect/factories.py b/src/py/mat3ra/made/tools/build/defect/factories.py index d9fdd033..dd394eea 100644 --- a/src/py/mat3ra/made/tools/build/defect/factories.py +++ b/src/py/mat3ra/made/tools/build/defect/factories.py @@ -6,6 +6,7 @@ class DefectBuilderFactory(BaseFactory): "vacancy": "mat3ra.made.tools.build.defect.builders.VacancyPointDefectBuilder", "substitution": "mat3ra.made.tools.build.defect.builders.SubstitutionPointDefectBuilder", "interstitial": "mat3ra.made.tools.build.defect.builders.InterstitialPointDefectBuilder", + "interstitial:voronoi_site": "mat3ra.made.tools.build.defect.builders.VoronoiInterstitialPointDefectBuilder", "adatom:coordinate": "mat3ra.made.tools.build.defect.builders.AdatomSlabDefectBuilder", "adatom:crystal_site": "mat3ra.made.tools.build.defect.builders.CrystalSiteAdatomSlabDefectBuilder", "adatom:equidistant": "mat3ra.made.tools.build.defect.builders.EquidistantAdatomSlabDefectBuilder", diff --git a/src/py/mat3ra/made/tools/third_party.py b/src/py/mat3ra/made/tools/third_party.py index e862e2e7..9467989e 100644 --- a/src/py/mat3ra/made/tools/third_party.py +++ b/src/py/mat3ra/made/tools/third_party.py @@ -17,6 +17,7 @@ from pymatgen.analysis.defects.core import Interstitial as PymatgenInterstitial from pymatgen.analysis.defects.core import Substitution as PymatgenSubstitution from pymatgen.analysis.defects.core import Vacancy as PymatgenVacancy +from pymatgen.analysis.defects.generators import VoronoiInterstitialGenerator as PymatgenVoronoiInterstitialGenerator from pymatgen.analysis.local_env import VoronoiNN as PymatgenVoronoiNN from pymatgen.core import IStructure as PymatgenIStructure from pymatgen.core import PeriodicSite as PymatgenPeriodicSite @@ -50,6 +51,7 @@ "PymatgenVacancy", "PymatgenSubstitution", "PymatgenInterstitial", + "PymatgenVoronoiInterstitialGenerator", "label_pymatgen_slab_termination", "ase_make_supercell", "ase_add_vacuum", diff --git a/tests/py/unit/test_tools_build_defect.py b/tests/py/unit/test_tools_build_defect.py index 27fe8647..5a9b62ee 100644 --- a/tests/py/unit/test_tools_build_defect.py +++ b/tests/py/unit/test_tools_build_defect.py @@ -60,6 +60,21 @@ def test_create_interstitial(): ] +def test_create_interstitial_voronoi(): + configuration = PointDefectConfiguration( + crystal=clean_material, + defect_type="interstitial", + chemical_element="Ge", + # Voronoi must resolve to [0.5, 0.5, 0.5] for Si structure + coordinate=[0.25, 0.25, 0.5], + placement_method="voronoi_site", + ) + defect = create_defect(configuration) + + assert defect.basis.elements.values[-1] == "Ge" + assertion_utils.assert_deep_almost_equal([0.5, 0.5, 0.5], defect.basis.coordinates.values[-1]) + + def test_create_defect_from_site_id(): # Substitution of Ge in place of Si at site_id=1 defect_configuration = PointDefectConfiguration.from_site_id(