diff --git a/src/py/mat3ra/made/tools/build/defect/builders.py b/src/py/mat3ra/made/tools/build/defect/builders.py index 0ab99396..95f7f9a5 100644 --- a/src/py/mat3ra/made/tools/build/defect/builders.py +++ b/src/py/mat3ra/made/tools/build/defect/builders.py @@ -115,18 +115,111 @@ class SlabDefectBuilder(DefectBuilder): _BuildParametersType = SlabDefectBuilderParameters _DefaultBuildParameters = SlabDefectBuilderParameters() - def create_material_with_additional_layers(self, material: Material, added_thickness: int = 1) -> Material: + def create_material_with_additional_layers( + self, material: Material, added_thickness: Union[int, float] = 1 + ) -> Material: + """ + Adds a number of layers to the material. + + Args: + material: The original material. + added_thickness: The thickness to add. + + Returns: + A new Material instance with the added layers. + """ + if isinstance(added_thickness, int): + return self.create_material_with_additional_layers_int(material, added_thickness) + elif isinstance(added_thickness, float): + return self.create_material_with_additional_layers_float(material, added_thickness) + else: + raise TypeError("added_thickness must be an integer or float for this method.") + + def create_material_with_additional_layers_int(self, material: Material, added_thickness: int = 1) -> Material: + """ + Adds an integer number of layers to the material. + + Args: + material: The original material. + added_thickness: The number of whole layers to add. + + Returns: + A new Material instance with the added layers. + """ + new_material = material.clone() termination = Termination.from_string(new_material.metadata.get("build").get("termination")) build_config = new_material.metadata.get("build").get("configuration") + if build_config["type"] != "SlabConfiguration": raise ValueError("Material is not a slab.") build_config.pop("type") build_config["thickness"] = build_config["thickness"] + added_thickness + new_slab_config = SlabConfiguration(**build_config) - material_with_additional_layer = create_slab(new_slab_config, termination) + material_with_additional_layers = create_slab(new_slab_config, termination) + + return material_with_additional_layers + + def create_material_with_additional_layers_float( + self, material: Material, added_thickness: float = 1.0 + ) -> Material: + """ + Adds a fractional number of layers to the material. + + Args: + material: The original material. + added_thickness: The fractional thickness to add. + + Returns: + A new Material instance with the fractional layer added. + """ + whole_layers = int(added_thickness) + fractional_part = added_thickness - whole_layers + + if whole_layers > 0: + material_with_additional_layers = self.create_material_with_additional_layers_int(material, whole_layers) + else: + material_with_additional_layers = material.clone() + + if fractional_part > 0.0: + material_with_additional_layers = self.add_fractional_layer( + material_with_additional_layers, whole_layers, fractional_part + ) + + return material_with_additional_layers + + def add_fractional_layer( + self, + material: Material, + whole_layers: int, + fractional_thickness: float, + ) -> Material: + """ + Adds a fractional layer to the material. + + Args: + material: The original material. + fractional_thickness: The fractional thickness to add. + + Returns: + A new Material instance with the fractional layer added. + """ + material_with_additional_layers = self.create_material_with_additional_layers_int(material, 1) + new_c = material_with_additional_layers.lattice.c + layer_height = (new_c - material.lattice.c) / (whole_layers + 1) + original_max_z = get_atomic_coordinates_extremum(material, "max", "z", use_cartesian_coordinates=True) + added_layers_max_z = original_max_z + (whole_layers + fractional_thickness) * layer_height + added_layers_max_z_crystal = material_with_additional_layers.basis.cell.convert_point_to_crystal( + [0, 0, added_layers_max_z] + )[2] + + material_with_additional_layers = filter_by_box( + material=material_with_additional_layers, + max_coordinate=[1, 1, added_layers_max_z_crystal], + ) - return material_with_additional_layer + return material_with_additional_layers def merge_slab_and_defect(self, material: Material, isolated_defect: Material) -> Material: new_vacuum = isolated_defect.lattice.c - material.lattice.c @@ -137,6 +230,9 @@ def merge_slab_and_defect(self, material: Material, isolated_defect: Material) - material_name=material.name, merge_dangerously=True, ) + new_material.to_crystal() + if self.build_parameters.auto_add_vacuum and get_atomic_coordinates_extremum(new_material, "max", "z") > 1: + new_material = add_vacuum(new_material, self.build_parameters.vacuum_thickness) return new_material diff --git a/src/py/mat3ra/made/tools/build/defect/configuration.py b/src/py/mat3ra/made/tools/build/defect/configuration.py index 33d62081..7eda27cd 100644 --- a/src/py/mat3ra/made/tools/build/defect/configuration.py +++ b/src/py/mat3ra/made/tools/build/defect/configuration.py @@ -1,4 +1,4 @@ -from typing import Optional, List, Union +from typing import Optional, List, Union, Generic, TypeVar from pydantic import BaseModel from mat3ra.code.entity import InMemoryEntity @@ -11,8 +11,15 @@ BoxCoordinateCondition, TriangularPrismCoordinateCondition, PlaneCoordinateCondition, + CoordinateCondition, +) +from .enums import ( + PointDefectTypeEnum, + SlabDefectTypeEnum, + AtomPlacementMethodEnum, + ComplexDefectTypeEnum, + CoordinatesShapeEnum, ) -from .enums import PointDefectTypeEnum, SlabDefectTypeEnum, AtomPlacementMethodEnum, ComplexDefectTypeEnum class BaseDefectConfiguration(BaseModel): @@ -109,10 +116,10 @@ class SlabDefectConfiguration(BaseDefectConfiguration, InMemoryEntity): Args: crystal (Material): The Material object. - number_of_added_layers (int): The number of added layers. + number_of_added_layers (Union[int, float]): The number of added layers to the slab. """ - number_of_added_layers: int = 1 + number_of_added_layers: Union[int, float] = 1 @property def _json(self): @@ -188,7 +195,10 @@ def _json(self): } -class IslandSlabDefectConfiguration(SlabDefectConfiguration): +CoordinateConditionType = TypeVar("CoordinateConditionType", bound=CoordinateCondition) + + +class IslandSlabDefectConfiguration(SlabDefectConfiguration, Generic[CoordinateConditionType]): """ Configuration for an island slab defect. @@ -207,8 +217,60 @@ class IslandSlabDefectConfiguration(SlabDefectConfiguration): BoxCoordinateCondition, TriangularPrismCoordinateCondition, PlaneCoordinateCondition, + CoordinateConditionType, ] = CylinderCoordinateCondition() + @classmethod + def from_dict(cls, crystal: Material, condition: dict, **kwargs): + """ + Creates an IslandSlabDefectConfiguration instance from a dictionary. + + Args: + crystal (Material): The material object. + condition (dict): The dictionary with shape and other parameters for the condition. + kwargs: Other configuration parameters (like number_of_added_layers). + """ + condition_obj = cls.get_coordinate_condition(shape=condition["shape"], dict_params=condition) + return cls(crystal=crystal, condition=condition_obj, **kwargs) + + @staticmethod + def get_coordinate_condition(shape: CoordinatesShapeEnum, dict_params: dict): + """ + Returns the appropriate coordinate condition based on the shape provided. + + Args: + shape (CoordinatesShapeEnum): Shape of the island (e.g., cylinder, box, etc.). + dict_params (dict): Parameters for the shape condition. + + Returns: + CoordinateCondition: The appropriate condition object. + """ + if shape == "cylinder": + return CylinderCoordinateCondition( + center_position=dict_params.get("center_position", [0.5, 0.5]), + radius=dict_params["radius"], + min_z=dict_params["min_z"], + max_z=dict_params["max_z"], + ) + elif shape == "sphere": + return SphereCoordinateCondition( + center_position=dict_params.get("center_position", [0.5, 0.5]), radius=dict_params["radius"] + ) + elif shape == "box": + return BoxCoordinateCondition( + min_coordinate=dict_params["min_coordinate"], max_coordinate=dict_params["max_coordinate"] + ) + elif shape == "triangular_prism": + return TriangularPrismCoordinateCondition( + position_on_surface_1=dict_params["position_on_surface_1"], + position_on_surface_2=dict_params["position_on_surface_2"], + position_on_surface_3=dict_params["position_on_surface_3"], + min_z=dict_params["min_z"], + max_z=dict_params["max_z"], + ) + else: + raise ValueError(f"Unsupported island shape: {shape}") + @property def _json(self): return { diff --git a/src/py/mat3ra/made/tools/build/defect/enums.py b/src/py/mat3ra/made/tools/build/defect/enums.py index a169ad69..bdc22279 100644 --- a/src/py/mat3ra/made/tools/build/defect/enums.py +++ b/src/py/mat3ra/made/tools/build/defect/enums.py @@ -26,3 +26,10 @@ 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" + + +class CoordinatesShapeEnum(str, Enum): + SPHERE = "sphere" + CYLINDER = "cylinder" + BOX = "rectangle" + TRIANGULAR_PRISM = "triangular_prism"