Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/SOF 7282 1 #163

Merged
merged 25 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
57fd8b5
feat: add commensurate supercells generation builder
VsevolodX Sep 19, 2024
81e03bf
Merge branch 'feature/SOF-7282' into feature/SOF-7282-1
VsevolodX Sep 19, 2024
fe58e85
update: adjustments to make work
VsevolodX Sep 19, 2024
07ff7c3
update: filter for angle
VsevolodX Sep 20, 2024
54e14b0
Merge branch 'main' into feature/SOF-7282-1
VsevolodX Sep 20, 2024
e3e7967
update: add check for commensurability on lattice vectors and angle
VsevolodX Sep 21, 2024
4ee4e53
chore: types and cleanups
VsevolodX Sep 21, 2024
e40d872
update: add check for pure rotation
VsevolodX Sep 21, 2024
405d1fa
update: move mateices func to utils
VsevolodX Sep 23, 2024
e0a4f00
updaete: move matrix -> angle func to utils
VsevolodX Sep 23, 2024
f563b20
chore: cleanup imports
VsevolodX Sep 23, 2024
693d946
update: move try loop into func
VsevolodX Sep 23, 2024
71bcb7a
chore: cleanup commensurate lattices
VsevolodX Sep 23, 2024
4ece86b
chore: type fix
VsevolodX Sep 23, 2024
e41c255
update: move configs to configs
VsevolodX Sep 23, 2024
cf8b0f5
update: create commlat pair file
VsevolodX Sep 23, 2024
01fd927
use new structures + types
VsevolodX Sep 23, 2024
b7650e7
update: fix failed branching logic
VsevolodX Sep 23, 2024
da3a7e0
update: add test verified in nb
VsevolodX Sep 23, 2024
4383c76
chore: fix test
VsevolodX Sep 23, 2024
30f5a4c
chore: cleanups
VsevolodX Sep 23, 2024
5ff26a9
chore: cleanups 2
VsevolodX Sep 23, 2024
f29463a
update: add actual angle metadata + test that
VsevolodX Sep 23, 2024
7f803d2
chore: adjust description
VsevolodX Sep 24, 2024
71b98a3
chore: add angle units
VsevolodX Sep 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 137 additions & 68 deletions src/py/mat3ra/made/tools/build/interface/builders.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,42 @@
from typing import Any, List, Optional

import numpy as np
from ..utils import merge_materials
from ...modify import (
translate_to_z_level,
rotate_material,
translate_by_vector,
add_vacuum_sides,
)
from pydantic import BaseModel, Field
from pydantic import BaseModel
from ase.build.tools import niggli_reduce
from pymatgen.analysis.interfaces.coherent_interfaces import (
CoherentInterfaceBuilder,
ZSLGenerator,
)

from ..nanoribbon import NanoribbonConfiguration, create_nanoribbon

from mat3ra.made.material import Material
from .enums import StrainModes
from .configuration import InterfaceConfiguration
from .termination_pair import TerminationPair, safely_select_termination_pair
from .utils import interface_patch_with_mean_abs_strain, remove_duplicate_interfaces
from ....utils import create_2d_supercell_matrices, get_angle_from_rotation_matrix_2d
from ...modify import (
translate_to_z_level,
rotate_material,
translate_by_vector,
add_vacuum_sides,
)
from ...analyze import get_chemical_formula
from ...convert import to_ase, from_ase, to_pymatgen, PymatgenInterface, ASEAtoms
from ...build import BaseBuilder
from ..nanoribbon import NanoribbonConfiguration, create_nanoribbon
from ..supercell import create_supercell
from ..slab import create_slab, Termination, SlabConfiguration
from ..utils import merge_materials
from ..mixins import (
ConvertGeneratedItemsASEAtomsMixin,
ConvertGeneratedItemsPymatgenStructureMixin,
)
from ..slab import create_slab, Termination
from ..slab.configuration import SlabConfiguration
from ...analyze import get_chemical_formula
from ...convert import to_ase, from_ase, to_pymatgen, PymatgenInterface, ASEAtoms
from ...build import BaseBuilder, BaseConfiguration

from .enums import StrainModes
from .configuration import (
InterfaceConfiguration,
NanoRibbonTwistedInterfaceConfiguration,
TwistedInterfaceConfiguration,
)
from .commensurate_lattice_pair import CommensurateLatticePair
from .termination_pair import TerminationPair, safely_select_termination_pair
from .utils import interface_patch_with_mean_abs_strain, remove_duplicate_interfaces


class InterfaceBuilderParameters(BaseModel):
Expand Down Expand Up @@ -202,60 +208,15 @@ def _post_process(self, items: List[_GeneratedItemType], post_process_parameters
########################################################################################
# Twisted Interface Builders #
########################################################################################
class TwistedInterfaceConfiguration(BaseConfiguration):
film: Material
substrate: Material
twist_angle: float = Field(0, description="Twist angle in degrees")
distance_z: float = 3.0

@property
def _json(self):
return {
"type": self.get_cls_name(),
"film": self.film.to_json(),
"substrate": self.substrate.to_json(),
"twist_angle": self.twist_angle,
"distance_z": self.distance_z,
}


class NanoRibbonTwistedInterfaceConfiguration(TwistedInterfaceConfiguration):
"""
Configuration for creating a twisted interface between two nano ribbons with specified twist angle.

Args:
film (Material): The film material.
substrate (Material): The substrate material.
twist_angle (float): Twist angle in degrees.
ribbon_width (int): Width of the nanoribbon in unit cells.
ribbon_length (int): Length of the nanoribbon in unit cells.
distance_z (float): Vertical distance between layers in Angstroms.
vacuum_x (float): Vacuum along x on both sides, in Angstroms.
vacuum_y (float): Vacuum along y on both sides, in Angstroms.
"""

ribbon_width: int = 1
ribbon_length: int = 1
vacuum_x: float = 5.0
vacuum_y: float = 5.0

@property
def _json(self):
return {
**super()._json,
"type": self.get_cls_name(),
"ribbon_width": self.ribbon_width,
"ribbon_length": self.ribbon_length,
"vacuum_x": self.vacuum_x,
"vacuum_y": self.vacuum_y,
}


class NanoRibbonTwistedInterfaceBuilder(BaseBuilder):
_GeneratedItemType = Material
_ConfigurationType = NanoRibbonTwistedInterfaceConfiguration
_ConfigurationType: type( # type: ignore
NanoRibbonTwistedInterfaceConfiguration
) = NanoRibbonTwistedInterfaceConfiguration # type: ignore

def _generate(self, configuration: NanoRibbonTwistedInterfaceConfiguration) -> List[Material]:
def _generate(self, configuration: _ConfigurationType) -> List[Material]:
bottom_nanoribbon_configuration = NanoribbonConfiguration(
material=configuration.substrate,
width=configuration.ribbon_width,
Expand Down Expand Up @@ -284,3 +245,111 @@ def _update_material_name(
) -> Material:
material.name = f"Twisted Nanoribbon Interface ({configuration.twist_angle:.2f}°)"
return material


class CommensurateLatticeInterfaceBuilderParameters(BaseModel):
"""
Parameters for the commensurate lattice interface builder.

Args:
max_repetition_int (int): The maximum search range for commensurate lattices.
angle_tolerance (float): The tolerance for the angle between the commensurate lattice and the target angle.
return_first_match (bool): Whether to return the first match or all matches.
"""

max_repetition_int: int = 10
angle_tolerance: float = 0.1
return_first_match: bool = False


class CommensurateLatticeInterfaceBuilder(BaseBuilder):
_GeneratedItemType: type(CommensurateLatticePair) = CommensurateLatticePair # type: ignore
_ConfigurationType: type(TwistedInterfaceConfiguration) = TwistedInterfaceConfiguration # type: ignore

def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]:
film = configuration.film
# substrate = configuration.substrate
max_search = self.build_parameters.max_repetition_int
a = film.lattice.vector_arrays[0][:2]
b = film.lattice.vector_arrays[1][:2]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have a getter for this film.lattice.a, film.lattice.b, film.lattice.c

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. We're adding this across all the made during cleanup task.

commensurate_lattice_pairs = self.__generate_commensurate_lattices(
configuration, a, b, max_search, configuration.twist_angle
)
return commensurate_lattice_pairs

def __generate_commensurate_lattices(
self,
configuration: TwistedInterfaceConfiguration,
a: List[float],
b: List[float],
max_search: int = 10,
target_angle: float = 0.0,
) -> List[CommensurateLatticePair]:
"""
Generate all commensurate lattices for a given search range and filter by closeness to target angle.

Args:
configuration (TwistedInterfaceConfiguration): The configuration for the twisted interface.
a (List[float]): The a lattice vector.
b (List[float]): The b lattice vector.
max_search (int): The maximum search range.
target_angle (float): The target angle.

Returns:
List[CommensurateLatticePair]: The list of commensurate lattice pairs
"""
matrices = create_2d_supercell_matrices(max_search)
matrix_ab = np.array([a, b])
matrix_ab_inverse = np.linalg.inv(matrix_ab)

solutions: List[CommensurateLatticePair] = []
for index1, matrix1 in enumerate(matrices):
for index2, matrix2 in enumerate(matrices[0 : index1 + 1]):
matrix2_inverse = np.linalg.inv(matrix2)
intermediate_product = matrix2_inverse @ matrix1
product = matrix_ab_inverse @ intermediate_product @ matrix_ab
angle = get_angle_from_rotation_matrix_2d(product)
if angle is not None:
size_metric = np.linalg.det(matrix_ab_inverse @ matrix1 @ matrix_ab)

if np.abs(angle - target_angle) < self.build_parameters.angle_tolerance:
print(f"Found commensurate lattice with angle {angle} and size metric {size_metric}")
solutions.append(
timurbazhirov marked this conversation as resolved.
Show resolved Hide resolved
CommensurateLatticePair(
configuration=configuration,
matrix1=matrix1,
matrix2=matrix2,
angle=angle,
size_metric=size_metric,
)
)
if self.build_parameters.return_first_match:
return solutions
else:
continue
return solutions

def _post_process(
self,
items: List[_GeneratedItemType],
post_process_parameters=None,
) -> List[Material]:
interfaces = []
for item in items:
new_substrate = create_supercell(item.configuration.film, item.matrix1.tolist())
new_film = create_supercell(item.configuration.substrate, item.matrix2.tolist())
new_film = translate_by_vector(
new_film, [0, 0, item.configuration.distance_z], use_cartesian_coordinates=True
)
interface = merge_materials([new_substrate, new_film])
interface.metadata["actual_twist_angle"] = item.angle
interfaces.append(interface)
return interfaces

def _update_material_metadata(self, material, configuration) -> Material:
updated_material = super()._update_material_metadata(material, configuration)
if "actual_twist_angle" in material.metadata:
updated_material.metadata["build"]["configuration"]["actual_twist_angle"] = material.metadata[
"actual_twist_angle"
]
return updated_material
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import numpy as np
from pydantic import BaseModel

from .configuration import TwistedInterfaceConfiguration


class CommensurateLatticePair(BaseModel):
"""
Commensurate lattice pair model.

Attributes:
configuration (TwistedInterfaceConfiguration): The configuration of the twisted interface.
matrix1 (np.ndarray): The supercell matrix for the first lattice.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2d supercell matrix

matrix2 (np.ndarray): The supercell matrix for the second lattice.
angle (float): The angle between the two lattices, in degrees.
size_metric (float): The size metric of the resulting supercell, in arbitrary units.
"""

class Config:
timurbazhirov marked this conversation as resolved.
Show resolved Hide resolved
arbitrary_types_allowed = True

configuration: TwistedInterfaceConfiguration
matrix1: np.ndarray
matrix2: np.ndarray
angle: float
size_metric: float
51 changes: 51 additions & 0 deletions src/py/mat3ra/made/tools/build/interface/configuration.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from mat3ra.code.entity import InMemoryEntity
from pydantic import BaseModel

from mat3ra.made.material import Material
from .termination_pair import TerminationPair
from .. import BaseConfiguration
from ..slab import Termination
from ..slab.configuration import SlabConfiguration

Expand Down Expand Up @@ -29,3 +31,52 @@ def _json(self):
"distance_z": self.distance_z,
"vacuum": self.vacuum,
}


class TwistedInterfaceConfiguration(BaseConfiguration):
film: Material
substrate: Material
twist_angle: float = 0.0
distance_z: float = 3.0

@property
def _json(self):
return {
"type": self.get_cls_name(),
"film": self.film.to_json(),
"substrate": self.substrate.to_json(),
"twist_angle": self.twist_angle,
"distance_z": self.distance_z,
}


class NanoRibbonTwistedInterfaceConfiguration(TwistedInterfaceConfiguration):
"""
Configuration for creating a twisted interface between two nano ribbons with specified twist angle.

Args:
film (Material): The film material.
substrate (Material): The substrate material.
twist_angle (float): Twist angle in degrees.
ribbon_width (int): Width of the nanoribbon in unit cells.
ribbon_length (int): Length of the nanoribbon in unit cells.
distance_z (float): Vertical distance between layers in Angstroms.
vacuum_x (float): Vacuum along x on both sides, in Angstroms.
vacuum_y (float): Vacuum along y on both sides, in Angstroms.
"""

ribbon_width: int = 1
ribbon_length: int = 1
vacuum_x: float = 5.0
vacuum_y: float = 5.0

@property
def _json(self):
return {
**super()._json,
"type": self.get_cls_name(),
"ribbon_width": self.ribbon_width,
"ribbon_length": self.ribbon_length,
"vacuum_x": self.vacuum_x,
"vacuum_y": self.vacuum_y,
}
55 changes: 55 additions & 0 deletions src/py/mat3ra/made/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,61 @@ def get_overlapping_coordinates(
return [c for c in coordinates if np.linalg.norm(np.array(c) - np.array(coordinate)) < threshold]


def create_2d_supercell_matrices(max_search: int) -> List[np.ndarray]:
"""
Create a list of 2D supercell matrices within a maximum search range.

Filtering conditions:
- Non-zero area constraint
- Positive determinant (to exclude mirroring transformations)

Args:
max_search: The maximum search range.
Returns:
List[np.ndarray]: The list of supercell matrices.
"""
matrices = []
for s11 in range(-max_search, max_search + 1):
for s12 in range(-max_search, max_search + 1):
for s21 in range(-max_search, max_search + 1):
for s22 in range(-max_search, max_search + 1):
matrix = np.array([[s11, s12], [s21, s22]])
determinant = np.linalg.det(matrix)
if determinant == 0 or determinant < 0:
timurbazhirov marked this conversation as resolved.
Show resolved Hide resolved
continue
matrices.append(matrix)
return matrices


def get_angle_from_rotation_matrix_2d(
matrix: np.ndarray, zero_tolerance: float = 1e-6, round_digits: int = 3
) -> Union[float, None]:
"""
Get the angle from a 2x2 rotation matrix in degrees if it's a pure rotation matrix.
Args:
matrix: The 2x2 rotation matrix.
zero_tolerance: The zero tolerance for the determinant.
round_digits: The number of digits to round the angle.

Returns:
Union[float, None]: The angle in degrees if it's a pure rotation matrix, otherwise None.
"""
if matrix.shape != (2, 2):
return None
if np.abs(np.linalg.det(matrix) - 1) > zero_tolerance:
return None
if not np.all(np.abs(matrix) <= 1):
return None
# Check if it's in form of rotation matrix [cos(theta), -sin(theta); sin(theta), cos(theta)]
if not np.allclose(matrix @ matrix.T, np.eye(2), atol=zero_tolerance):
return None
cos_theta = matrix[0, 0]
sin_theta = matrix[1, 0]
angle_rad = np.arctan2(sin_theta, cos_theta)
angle_deg = np.round(np.degrees(angle_rad), round_digits)
return angle_deg


class ValueWithId(RoundNumericValuesMixin, BaseModel):
id: int = 0
value: Any = None
Expand Down
Loading
Loading