Skip to content

Commit

Permalink
Merge pull request #164 from Exabyte-io/feature/SOF-7433-1
Browse files Browse the repository at this point in the history
feature/SOF-7433-1 feat: surface grain boundary builder
  • Loading branch information
VsevolodX authored Oct 1, 2024
2 parents 575ba87 + b1a93c2 commit bad57b6
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 20 deletions.
13 changes: 8 additions & 5 deletions src/py/mat3ra/made/tools/build/grain_boundary/__init__.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
58 changes: 54 additions & 4 deletions src/py/mat3ra/made/tools/build/grain_boundary/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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
24 changes: 23 additions & 1 deletion src/py/mat3ra/made/tools/build/grain_boundary/configuration.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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,
}
4 changes: 2 additions & 2 deletions src/py/mat3ra/made/tools/build/interface/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ def _update_material_name(
return material


class CommensurateLatticeInterfaceBuilderParameters(BaseModel):
class CommensurateLatticeTwistedInterfaceBuilderParameters(BaseModel):
"""
Parameters for the commensurate lattice interface builder.
Expand All @@ -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

Expand Down
6 changes: 4 additions & 2 deletions src/py/mat3ra/made/tools/build/interface/configuration.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Optional

from mat3ra.code.entity import InMemoryEntity
from pydantic import BaseModel

Expand Down Expand Up @@ -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

Expand All @@ -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,
}
Expand Down
91 changes: 91 additions & 0 deletions src/py/mat3ra/made/tools/build/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -78,10 +81,98 @@ 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(
merged_material, material, material_name, distance_tolerance, merge_dangerously
)

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
36 changes: 34 additions & 2 deletions tests/py/unit/test_tools_build_grain_boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 = [
Expand All @@ -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)
8 changes: 4 additions & 4 deletions tests/py/unit/test_tools_build_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
create_interfaces,
)
from mat3ra.made.tools.build.interface.builders import (
CommensurateLatticeInterfaceBuilder,
CommensurateLatticeInterfaceBuilderParameters,
CommensurateLatticeTwistedInterfaceBuilder,
CommensurateLatticeTwistedInterfaceBuilderParameters,
NanoRibbonTwistedInterfaceBuilder,
NanoRibbonTwistedInterfaceConfiguration,
TwistedInterfaceConfiguration,
Expand Down Expand Up @@ -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]
Expand Down

0 comments on commit bad57b6

Please sign in to comment.