diff --git a/src/py/mat3ra/made/tools/build/grain_boundary/__init__.py b/src/py/mat3ra/made/tools/build/grain_boundary/__init__.py index 504766ad..c051e959 100644 --- a/src/py/mat3ra/made/tools/build/grain_boundary/__init__.py +++ b/src/py/mat3ra/made/tools/build/grain_boundary/__init__.py @@ -1,14 +1,17 @@ -from typing import Optional +from typing import Optional, Union from mat3ra.made.material import Material -from .builders import SlabGrainBoundaryBuilder -from .configuration import SlabGrainBoundaryConfiguration +from .builders import SlabGrainBoundaryBuilder, SurfaceGrainBoundaryBuilder, SurfaceGrainBoundaryBuilderParameters +from .configuration import ( + SlabGrainBoundaryConfiguration, + SurfaceGrainBoundaryConfiguration, +) def create_grain_boundary( - configuration: SlabGrainBoundaryConfiguration, - builder: Optional[SlabGrainBoundaryBuilder] = None, + configuration: Union[SlabGrainBoundaryConfiguration, SurfaceGrainBoundaryConfiguration], + builder: Union[SlabGrainBoundaryBuilder, SurfaceGrainBoundaryBuilder, None] = None, ) -> Material: """ Create a grain boundary according to provided configuration with selected builder. diff --git a/src/py/mat3ra/made/tools/build/grain_boundary/builders.py b/src/py/mat3ra/made/tools/build/grain_boundary/builders.py index 3db6d003..d3621bf5 100644 --- a/src/py/mat3ra/made/tools/build/grain_boundary/builders.py +++ b/src/py/mat3ra/made/tools/build/grain_boundary/builders.py @@ -3,13 +3,19 @@ import numpy as np from mat3ra.made.material import Material -from ..slab import SlabConfiguration, get_terminations, create_slab +from ...third_party import PymatgenInterface from ...analyze import get_chemical_formula +from ..slab import SlabConfiguration, get_terminations, create_slab from ..interface import ZSLStrainMatchingInterfaceBuilderParameters, InterfaceConfiguration -from ..interface.builders import ZSLStrainMatchingInterfaceBuilder + +from ..interface.builders import ( + ZSLStrainMatchingInterfaceBuilder, + CommensurateLatticeTwistedInterfaceBuilder, + CommensurateLatticeTwistedInterfaceBuilderParameters, +) from ..supercell import create_supercell -from .configuration import SlabGrainBoundaryConfiguration -from ...third_party import PymatgenInterface +from ..utils import stack_two_materials_xy +from .configuration import SurfaceGrainBoundaryConfiguration, SlabGrainBoundaryConfiguration class SlabGrainBoundaryBuilderParameters(ZSLStrainMatchingInterfaceBuilderParameters): @@ -27,6 +33,7 @@ class SlabGrainBoundaryBuilder(ZSLStrainMatchingInterfaceBuilder): """ _BuildParametersType: type(SlabGrainBoundaryBuilderParameters) = SlabGrainBoundaryBuilderParameters # type: ignore + _DefaultBuildParameters = SlabGrainBoundaryBuilderParameters() _ConfigurationType: type(SlabGrainBoundaryConfiguration) = SlabGrainBoundaryConfiguration # type: ignore _GeneratedItemType: PymatgenInterface = PymatgenInterface # type: ignore selector_parameters: type( # type: ignore @@ -78,3 +85,46 @@ def _update_material_name(self, material: Material, configuration: _Configuratio ) material.name = new_name return material + + +class SurfaceGrainBoundaryBuilderParameters(CommensurateLatticeTwistedInterfaceBuilderParameters): + """ + Parameters for creating a grain boundary between two surface phases. + + Args: + edge_inclusion_tolerance (float): The tolerance to include atoms on the edge of each phase, in angstroms. + distance_tolerance (float): The distance tolerance to remove atoms that are too close, in angstroms. + """ + + edge_inclusion_tolerance: float = 1.0 + distance_tolerance: float = 1.0 + + +class SurfaceGrainBoundaryBuilder(CommensurateLatticeTwistedInterfaceBuilder): + _ConfigurationType: type(SurfaceGrainBoundaryConfiguration) = SurfaceGrainBoundaryConfiguration # type: ignore + _BuildParametersType = SurfaceGrainBoundaryBuilderParameters + _DefaultBuildParameters = SurfaceGrainBoundaryBuilderParameters() + + def _post_process(self, items: List[Material], post_process_parameters=None) -> List[Material]: + grain_boundaries = [] + for item in items: + matrix1 = np.dot(np.array(item.configuration.xy_supercell_matrix), item.matrix1) + matrix2 = np.dot(np.array(item.configuration.xy_supercell_matrix), item.matrix2) + phase_1_material_initial = create_supercell(item.configuration.film, matrix1.tolist()) + phase_2_material_initial = create_supercell(item.configuration.film, matrix2.tolist()) + + interface = stack_two_materials_xy( + phase_1_material_initial, + phase_2_material_initial, + gap=item.configuration.gap, + edge_inclusion_tolerance=self.build_parameters.edge_inclusion_tolerance, + distance_tolerance=self.build_parameters.distance_tolerance, + ) + grain_boundaries.append(interface) + + return grain_boundaries + + def _update_material_name(self, material: Material, configuration: SurfaceGrainBoundaryConfiguration) -> Material: + new_name = f"{configuration.film.name}, Grain Boundary ({configuration.twist_angle:.2f}°)" + material.name = new_name + return material diff --git a/src/py/mat3ra/made/tools/build/grain_boundary/configuration.py b/src/py/mat3ra/made/tools/build/grain_boundary/configuration.py index 338e27d7..f9980458 100644 --- a/src/py/mat3ra/made/tools/build/grain_boundary/configuration.py +++ b/src/py/mat3ra/made/tools/build/grain_boundary/configuration.py @@ -1,8 +1,9 @@ -from typing import Optional +from typing import Optional, List from .. import BaseConfiguration from ..slab.configuration import SlabConfiguration from ..slab.termination import Termination +from ..interface.configuration import TwistedInterfaceConfiguration class SlabGrainBoundaryConfiguration(BaseConfiguration): @@ -38,3 +39,24 @@ def _json(self): "gap": self.gap, "slab_configuration": self.slab_configuration.to_json(), } + + +class SurfaceGrainBoundaryConfiguration(TwistedInterfaceConfiguration): + """ + Configuration for creating a surface grain boundary. + + Args: + gap (float): The gap between the two phases. + xy_supercell_matrix (List[List[int]]): The supercell matrix to apply for both phases. + """ + + gap: float = 0.0 + xy_supercell_matrix: List[List[int]] = [[1, 0], [0, 1]] + + @property + def _json(self): + return { + "type": self.get_cls_name(), + "gap": self.gap, + "xy_supercell_matrix": self.xy_supercell_matrix, + } diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index 70a09c84..f8a0e5ad 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -247,7 +247,7 @@ def _update_material_name( return material -class CommensurateLatticeInterfaceBuilderParameters(BaseModel): +class CommensurateLatticeTwistedInterfaceBuilderParameters(BaseModel): """ Parameters for the commensurate lattice interface builder. @@ -263,7 +263,7 @@ class CommensurateLatticeInterfaceBuilderParameters(BaseModel): return_first_match: bool = False -class CommensurateLatticeInterfaceBuilder(BaseBuilder): +class CommensurateLatticeTwistedInterfaceBuilder(BaseBuilder): _GeneratedItemType: type(CommensurateLatticePair) = CommensurateLatticePair # type: ignore _ConfigurationType: type(TwistedInterfaceConfiguration) = TwistedInterfaceConfiguration # type: ignore diff --git a/src/py/mat3ra/made/tools/build/interface/configuration.py b/src/py/mat3ra/made/tools/build/interface/configuration.py index 8d29f827..5269c7aa 100644 --- a/src/py/mat3ra/made/tools/build/interface/configuration.py +++ b/src/py/mat3ra/made/tools/build/interface/configuration.py @@ -1,3 +1,5 @@ +from typing import Optional + from mat3ra.code.entity import InMemoryEntity from pydantic import BaseModel @@ -35,7 +37,7 @@ def _json(self): class TwistedInterfaceConfiguration(BaseConfiguration): film: Material - substrate: Material + substrate: Optional[Material] = None twist_angle: float = 0.0 distance_z: float = 3.0 @@ -44,7 +46,7 @@ def _json(self): return { "type": self.get_cls_name(), "film": self.film.to_json(), - "substrate": self.substrate.to_json(), + "substrate": self.substrate.to_json() if self.substrate else None, "twist_angle": self.twist_angle, "distance_z": self.distance_z, } diff --git a/src/py/mat3ra/made/tools/build/utils.py b/src/py/mat3ra/made/tools/build/utils.py index 6c89b2c5..c57c9df6 100644 --- a/src/py/mat3ra/made/tools/build/utils.py +++ b/src/py/mat3ra/made/tools/build/utils.py @@ -3,6 +3,9 @@ from typing import List, Optional from mat3ra.made.basis import Basis from mat3ra.made.material import Material + +from .supercell import create_supercell +from ..modify import filter_by_box, translate_by_vector from ...utils import ArrayWithIds @@ -78,6 +81,20 @@ def merge_materials( distance_tolerance: float = 0.01, merge_dangerously=False, ) -> Material: + """ + Merge multiple materials into a single material. + + If some of the atoms are considered too close within a tolerance, only the last atom is kept. + + Args: + materials (List[Material]): List of materials to merge. + material_name (Optional[str]): Name of the merged material. + distance_tolerance (float): The tolerance to replace atoms that are considered too close with respect + to the coordinates in the last material in the list, in angstroms. + merge_dangerously (bool): If True, the lattices are merged "as is" with no sanity checks. + Returns: + Material: The merged material. + """ merged_material = materials[0] for material in materials[1:]: merged_material = merge_two_materials( @@ -85,3 +102,77 @@ def merge_materials( ) return merged_material + + +def double_and_filter_material(material: Material, start: List[float], end: List[float]) -> Material: + """ + Double the material and filter it by a box defined by the start and end coordinates. + Args: + material (Material): The material to double and filter. + start (List[float]): The start coordinates of the box. + end (List[float]): The end coordinates of the box. + Returns: + Material: The filtered material. + """ + material_doubled = create_supercell(material, scaling_factor=[2, 1, 1]) + return filter_by_box(material_doubled, start, end) + + +def expand_lattice_vectors(material: Material, gap: float, direction: int = 0) -> Material: + """ + Expand the lattice vectors of the material in the specified direction by the given gap. + + Args: + material (Material): The material whose lattice vectors are to be expanded. + gap (float): The gap by which to expand the lattice vector. + direction (int): The index of the lattice vector to expand (0, 1, or 2). + """ + new_lattice_vectors = material.lattice.vector_arrays + new_lattice_vectors[direction][direction] += gap + material.set_new_lattice_vectors( + lattice_vector1=new_lattice_vectors[0], + lattice_vector2=new_lattice_vectors[1], + lattice_vector3=new_lattice_vectors[2], + ) + return material + + +def stack_two_materials_xy( + phase_1_material: Material, + phase_2_material: Material, + gap: float, + edge_inclusion_tolerance: Optional[float] = 1.0, + distance_tolerance: float = 1.0, +) -> Material: + """ + Stack two materials laterally with translation along x-axis with a gap between them. + + Works correctly only for materials with the same lattice vectors (commensurate lattices). + Args: + phase_1_material (Material): The first material. + phase_2_material (Material): The second material. + gap (float): The gap between the two materials, in angstroms. + edge_inclusion_tolerance (float): The tolerance to include atoms on the edge of the phase, in angstroms. + distance_tolerance (float): The distance tolerance to remove atoms that are too close, in angstroms. + + Returns: + Material: The merged material. + """ + edge_inclusion_tolerance_crystal = abs( + phase_1_material.basis.cell.convert_point_to_crystal([edge_inclusion_tolerance, 0, 0])[0] + ) + + phase_1_material = double_and_filter_material( + phase_1_material, [0 - edge_inclusion_tolerance_crystal, 0, 0], [0.5 + edge_inclusion_tolerance_crystal, 1, 1] + ) + + phase_2_material = double_and_filter_material( + phase_2_material, [0.5 - edge_inclusion_tolerance_crystal, 0, 0], [1 + edge_inclusion_tolerance_crystal, 1, 1] + ) + + phase_1_material = expand_lattice_vectors(phase_1_material, gap) + phase_2_material = expand_lattice_vectors(phase_2_material, gap) + + phase_2_material = translate_by_vector(phase_2_material, [gap / 2, 0, 0], use_cartesian_coordinates=True) + interface = merge_materials([phase_1_material, phase_2_material], distance_tolerance=distance_tolerance) + return interface diff --git a/tests/py/unit/test_tools_build_grain_boundary.py b/tests/py/unit/test_tools_build_grain_boundary.py index 6fcb797e..75fc5924 100644 --- a/tests/py/unit/test_tools_build_grain_boundary.py +++ b/tests/py/unit/test_tools_build_grain_boundary.py @@ -2,12 +2,17 @@ from mat3ra.made.tools.build.grain_boundary import ( SlabGrainBoundaryBuilder, SlabGrainBoundaryConfiguration, + SurfaceGrainBoundaryBuilder, + SurfaceGrainBoundaryBuilderParameters, + SurfaceGrainBoundaryConfiguration, create_grain_boundary, ) -from mat3ra.made.tools.build.interface import ZSLStrainMatchingInterfaceBuilderParameters +from mat3ra.made.tools.build.grain_boundary.builders import SlabGrainBoundaryBuilderParameters from mat3ra.made.tools.build.slab import SlabConfiguration, get_terminations from mat3ra.utils import assertion as assertion_utils +from .fixtures import GRAPHENE + def test_slab_grain_boundary_builder(): material = Material(Material.default_config) @@ -44,7 +49,7 @@ def test_slab_grain_boundary_builder(): slab_configuration=slab_config, ) - builder_params = ZSLStrainMatchingInterfaceBuilderParameters(max_area=50) + builder_params = SlabGrainBoundaryBuilderParameters() builder = SlabGrainBoundaryBuilder(build_parameters=builder_params) gb = create_grain_boundary(config, builder) expected_lattice_vectors = [ @@ -57,3 +62,30 @@ def test_slab_grain_boundary_builder(): assert len(gb.basis.elements.values) == 32 assertion_utils.assert_deep_almost_equal(expected_coordinate_15, gb.basis.coordinates.values[15]) assertion_utils.assert_deep_almost_equal(expected_lattice_vectors, gb.lattice.vector_arrays) + + +def test_create_surface_grain_boundary(): + config = SurfaceGrainBoundaryConfiguration( + film=Material(GRAPHENE), + twist_angle=13.0, + gap=2.0, + ) + + builder_params = SurfaceGrainBoundaryBuilderParameters( + max_repetition_int=5, + angle_tolerance=0.5, + return_first_match=True, + distance_tolerance=1.0, + ) + builder = SurfaceGrainBoundaryBuilder(build_parameters=builder_params) + + gb = builder.get_materials(config) + + expected_cell_vectors = [ + [23.509344266, 0.0, 0.0], + [5.377336066500001, 9.313819276550575, 0.0], + [0.0, 0.0, 20.0], + ] + + assert len(gb) == 1 + assertion_utils.assert_deep_almost_equal(expected_cell_vectors, gb[0].basis.cell.vectors_as_array) diff --git a/tests/py/unit/test_tools_build_interface.py b/tests/py/unit/test_tools_build_interface.py index f2680240..16ff5ec8 100644 --- a/tests/py/unit/test_tools_build_interface.py +++ b/tests/py/unit/test_tools_build_interface.py @@ -9,8 +9,8 @@ create_interfaces, ) from mat3ra.made.tools.build.interface.builders import ( - CommensurateLatticeInterfaceBuilder, - CommensurateLatticeInterfaceBuilderParameters, + CommensurateLatticeTwistedInterfaceBuilder, + CommensurateLatticeTwistedInterfaceBuilderParameters, NanoRibbonTwistedInterfaceBuilder, NanoRibbonTwistedInterfaceConfiguration, TwistedInterfaceConfiguration, @@ -72,10 +72,10 @@ def test_create_commensurate_supercell_twisted_interface(): film = Material(GRAPHENE) substrate = Material(GRAPHENE) config = TwistedInterfaceConfiguration(film=film, substrate=substrate, twist_angle=13, distance_z=3.0) - params = CommensurateLatticeInterfaceBuilderParameters( + params = CommensurateLatticeTwistedInterfaceBuilderParameters( max_repetition_int=5, angle_tolerance=0.5, return_first_match=True ) - builder = CommensurateLatticeInterfaceBuilder(build_parameters=params) + builder = CommensurateLatticeTwistedInterfaceBuilder(build_parameters=params) interfaces = builder.get_materials(config, post_process_parameters=config) assert len(interfaces) == 1 interface = interfaces[0]