From 57fd8b5247051fd58758e77e84dc9c78adacb0cf Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 18 Sep 2024 19:46:52 -0700 Subject: [PATCH 01/23] feat: add commensurate supercells generation builder --- .../made/tools/build/interface/builders.py | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index 0cc3f0ea..4c407847 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -287,3 +287,88 @@ def _update_material_name( ) -> Material: material.name = f"Twisted Nanoribbon Interface ({configuration.twist_angle:.2f}°)" return material + + +class CommensurateLatticeInterfaceBuilderParameters(BaseModel): + max_search: int = 10 + + +class CommensurateLatticeInterfaceBuilder(BaseBuilder): + _GeneratedItemType = Material + _ConfigurationType = InterfaceConfiguration + + def _generate(self, configuration: InterfaceConfiguration) -> List[Material]: + film = configuration.film_configuration.bulk + substrate = configuration.substrate_configuration.bulk + max_search = self.build_parameters.max_search + a1 = film.lattice.matrix[0] + a2 = film.lattice.matrix[1] + commensurate_lattices = self.__generate_commensurate_lattices(a1, a2, max_search) + interfaces = [] + for lattice in commensurate_lattices: + substrate_copy = substrate.copy() + substrate_copy.modify_lattice(lattice) + interface = merge_materials([film, substrate_copy]) + interfaces.append(interface) + return interfaces + + @staticmethod + def __create_matrices(max_search: int): + 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): + # Non-zero area constraint + matrix = np.array([[s11, s12], [s21, s22]]) + determinant = np.linalg.det(matrix) + # If matrices are degenerate or contain mirroring, skip + if determinant == 0 or determinant < 0: + continue + matrices.append(matrix) + return matrices + + @staticmethod + def __solve_angle_from_rotation_matrix(matrix, zero_tolerance=1e-6, round_digits=3): + if matrix.shape != (2, 2): + raise ValueError("Input matrix must be 2x2") + if np.abs(np.linalg.det(matrix) - 1) > zero_tolerance: + raise ValueError("Matrix must be orthogonal (determinant = 1)") + if not np.all(np.abs(matrix) <= 1): + raise ValueError("Matrix have all elements less than 1") + + # Extract the elements of the matrix + cos_theta = matrix[0, 0] + sin_theta = matrix[1, 0] + + # Calculate the angle in radians + angle_rad = np.arctan2(sin_theta, cos_theta) + + # Convert the angle to degrees + angle_deg = np.round(np.degrees(angle_rad), round_digits) + + return angle_deg + + def __generate_commensurate_lattices(self, a1: List[int], a2: List[int], max_search: int = 10): + """ + Generate all commensurate lattices for a given search range. + """ + matrices = self.__create_matrices(max_search) + matrix_a1a2 = np.array([a1, a2]) + matrix_a1a2_inverse = np.linalg.inv(matrix_a1a2) + + solutions = [] + 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_a1a2_inverse @ intermediate_product @ matrix_a1a2 + try: + angle = self.__solve_angle_from_rotation_matrix(product) + size_metric = np.linalg.det(matrix_a1a2_inverse @ matrix1 @ matrix_a1a2) + solutions.append( + {"matrix1": matrix1, "matrix2": matrix2, "angle": angle, "size_metric": size_metric} + ) + except ValueError: + continue + return solutions From fe58e854810965647ea463cc2a19313aa001e4ba Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:41:26 -0700 Subject: [PATCH 02/23] update: adjustments to make work --- .../made/tools/build/interface/builders.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index 15dd8117..acfceea6 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -1,6 +1,8 @@ from typing import Any, List, Optional import numpy as np +from mat3ra.made.tools.build.supercell import create_supercell + from ..utils import merge_materials from ...modify import ( translate_to_z_level, @@ -292,20 +294,21 @@ class CommensurateLatticeInterfaceBuilderParameters(BaseModel): class CommensurateLatticeInterfaceBuilder(BaseBuilder): _GeneratedItemType = Material - _ConfigurationType = InterfaceConfiguration + _ConfigurationType = TwistedInterfaceConfiguration - def _generate(self, configuration: InterfaceConfiguration) -> List[Material]: - film = configuration.film_configuration.bulk - substrate = configuration.substrate_configuration.bulk + def _generate(self, configuration: TwistedInterfaceConfiguration) -> List[Material]: + film = configuration.film + substrate = configuration.substrate max_search = self.build_parameters.max_search - a1 = film.lattice.matrix[0] - a2 = film.lattice.matrix[1] + a1 = film.lattice.vector_arrays[0][:2] + a2 = film.lattice.vector_arrays[1][:2] commensurate_lattices = self.__generate_commensurate_lattices(a1, a2, max_search) interfaces = [] for lattice in commensurate_lattices: - substrate_copy = substrate.copy() - substrate_copy.modify_lattice(lattice) - interface = merge_materials([film, substrate_copy]) + new_substrate = create_supercell(film, lattice["matrix1"]) + new_film = create_supercell(substrate, lattice["matrix2"]) + new_film = translate_by_vector(new_film, [0, 0, configuration.distance_z], use_cartesian_coordinates=True) + interface = merge_materials([new_substrate, new_film]) interfaces.append(interface) return interfaces @@ -346,7 +349,7 @@ def __solve_angle_from_rotation_matrix(matrix, zero_tolerance=1e-6, round_digits return angle_deg - def __generate_commensurate_lattices(self, a1: List[int], a2: List[int], max_search: int = 10): + def __generate_commensurate_lattices(self, a1: List[float], a2: List[float], max_search: int = 10): """ Generate all commensurate lattices for a given search range. """ From 07ff7c3dc71c605c3285c019e2aa4e7bd4203415 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:35:13 -0700 Subject: [PATCH 03/23] update: filter for angle --- .../made/tools/build/interface/builders.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index acfceea6..ab873b86 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -290,6 +290,7 @@ def _update_material_name( class CommensurateLatticeInterfaceBuilderParameters(BaseModel): max_search: int = 10 + angle_tolerance: float = 1.0 class CommensurateLatticeInterfaceBuilder(BaseBuilder): @@ -303,13 +304,21 @@ def _generate(self, configuration: TwistedInterfaceConfiguration) -> List[Materi a1 = film.lattice.vector_arrays[0][:2] a2 = film.lattice.vector_arrays[1][:2] commensurate_lattices = self.__generate_commensurate_lattices(a1, a2, max_search) + commensurate_lattices_with_angle = [ + lattice + for lattice in commensurate_lattices + if np.abs(lattice["angle"] - configuration.twist_angle) < self.build_parameters.angle_tolerance + ] interfaces = [] - for lattice in commensurate_lattices: - new_substrate = create_supercell(film, lattice["matrix1"]) - new_film = create_supercell(substrate, lattice["matrix2"]) + for lattice in commensurate_lattices_with_angle: + new_substrate = create_supercell(film, lattice["matrix1"].tolist()) + new_film = create_supercell(substrate, lattice["matrix2"].tolist()) new_film = translate_by_vector(new_film, [0, 0, configuration.distance_z], use_cartesian_coordinates=True) - interface = merge_materials([new_substrate, new_film]) - interfaces.append(interface) + try: + interface = merge_materials([new_substrate, new_film]) + interfaces.append(interface) + except Exception as e: + print(e) return interfaces @staticmethod @@ -322,7 +331,6 @@ def __create_matrices(max_search: int): # Non-zero area constraint matrix = np.array([[s11, s12], [s21, s22]]) determinant = np.linalg.det(matrix) - # If matrices are degenerate or contain mirroring, skip if determinant == 0 or determinant < 0: continue matrices.append(matrix) @@ -336,15 +344,9 @@ def __solve_angle_from_rotation_matrix(matrix, zero_tolerance=1e-6, round_digits raise ValueError("Matrix must be orthogonal (determinant = 1)") if not np.all(np.abs(matrix) <= 1): raise ValueError("Matrix have all elements less than 1") - - # Extract the elements of the matrix cos_theta = matrix[0, 0] sin_theta = matrix[1, 0] - - # Calculate the angle in radians angle_rad = np.arctan2(sin_theta, cos_theta) - - # Convert the angle to degrees angle_deg = np.round(np.degrees(angle_rad), round_digits) return angle_deg From e3e7967518a242190901b372ca36955f9c881fcf Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 20 Sep 2024 19:00:28 -0700 Subject: [PATCH 04/23] update: add check for commensurability on lattice vectors and angle --- .../made/tools/build/interface/builders.py | 93 ++++++++++++++----- 1 file changed, 70 insertions(+), 23 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index ab873b86..3acd0ca4 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -288,38 +288,38 @@ def _update_material_name( return material +class CommensurateLatticePair(BaseModel): + class Config: + arbitrary_types_allowed = True + + configuration: TwistedInterfaceConfiguration + matrix1: np.ndarray + matrix2: np.ndarray + angle: float + size_metric: float + + class CommensurateLatticeInterfaceBuilderParameters(BaseModel): max_search: int = 10 - angle_tolerance: float = 1.0 + angle_tolerance: float = 0.1 + return_first_match: bool = False class CommensurateLatticeInterfaceBuilder(BaseBuilder): - _GeneratedItemType = Material + _GeneratedItemType = CommensurateLatticePair _ConfigurationType = TwistedInterfaceConfiguration - def _generate(self, configuration: TwistedInterfaceConfiguration) -> List[Material]: + def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]: film = configuration.film substrate = configuration.substrate max_search = self.build_parameters.max_search a1 = film.lattice.vector_arrays[0][:2] a2 = film.lattice.vector_arrays[1][:2] - commensurate_lattices = self.__generate_commensurate_lattices(a1, a2, max_search) - commensurate_lattices_with_angle = [ - lattice - for lattice in commensurate_lattices - if np.abs(lattice["angle"] - configuration.twist_angle) < self.build_parameters.angle_tolerance + commensurate_lattices = self.__generate_commensurate_lattices(a1, a2, max_search, configuration.twist_angle) + commensurate_lattice_pairs = [ + CommensurateLatticePair(configuration=configuration, **lattice) for lattice in commensurate_lattices ] - interfaces = [] - for lattice in commensurate_lattices_with_angle: - new_substrate = create_supercell(film, lattice["matrix1"].tolist()) - new_film = create_supercell(substrate, lattice["matrix2"].tolist()) - new_film = translate_by_vector(new_film, [0, 0, configuration.distance_z], use_cartesian_coordinates=True) - try: - interface = merge_materials([new_substrate, new_film]) - interfaces.append(interface) - except Exception as e: - print(e) - return interfaces + return commensurate_lattice_pairs @staticmethod def __create_matrices(max_search: int): @@ -351,7 +351,9 @@ def __solve_angle_from_rotation_matrix(matrix, zero_tolerance=1e-6, round_digits return angle_deg - def __generate_commensurate_lattices(self, a1: List[float], a2: List[float], max_search: int = 10): + def __generate_commensurate_lattices( + self, a1: List[float], a2: List[float], max_search: int = 10, target_angle: float = 0.0 + ): """ Generate all commensurate lattices for a given search range. """ @@ -368,9 +370,54 @@ def __generate_commensurate_lattices(self, a1: List[float], a2: List[float], max try: angle = self.__solve_angle_from_rotation_matrix(product) size_metric = np.linalg.det(matrix_a1a2_inverse @ matrix1 @ matrix_a1a2) - solutions.append( - {"matrix1": matrix1, "matrix2": matrix2, "angle": angle, "size_metric": size_metric} - ) + + if np.abs(angle - target_angle) < self.build_parameters.angle_tolerance: + # We need to check if lattices of two materials will be the same by norm and angle + product1 = matrix1 @ matrix_a1a2 + product2 = matrix2 @ matrix_a1a2 + + norm_a1 = np.linalg.norm(product1[0]) + norm_b1 = np.linalg.norm(product1[1]) + norm_a2 = np.linalg.norm(product2[0]) + norm_b2 = np.linalg.norm(product2[1]) + + if np.isclose(norm_a1, norm_a2, atol=0.01) and np.isclose(norm_b1, norm_b2, atol=0.01): + print(matrix1, matrix2) + # let's check angle between a1 and b1 and a2 and b2, they should be the same + angle1 = np.arccos( + np.dot(product1[0], product1[1]) + / (np.linalg.norm(product1[0]) * np.linalg.norm(product1[1])) + ) + angle2 = np.arccos( + np.dot(product2[0], product2[1]) + / (np.linalg.norm(product2[0]) * np.linalg.norm(product2[1])) + ) + if np.isclose(angle1, angle2, atol=0.01): + print( + f"Found commensurate lattice with angle {angle} and size metric {size_metric} !!!!!!!" + ) + solutions.append( + {"matrix1": matrix1, "matrix2": matrix2, "angle": angle, "size_metric": size_metric} + ) + if self.build_parameters.return_first_match: + return solutions except ValueError: continue return solutions + + def _post_process( + self, items: List[_GeneratedItemType], post_process_parameters: Optional[_PostProcessParametersType] + ) -> 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 + ) + try: + interface = merge_materials([new_substrate, new_film]) + interfaces.append(interface) + except Exception as e: + print(e) + return interfaces From 4ee4e53969f3c6f244cd09cd41bc6304c1a6dbe8 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 20 Sep 2024 19:05:23 -0700 Subject: [PATCH 05/23] chore: types and cleanups --- .../made/tools/build/interface/builders.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index 3acd0ca4..a02784bf 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -306,16 +306,16 @@ class CommensurateLatticeInterfaceBuilderParameters(BaseModel): class CommensurateLatticeInterfaceBuilder(BaseBuilder): - _GeneratedItemType = CommensurateLatticePair - _ConfigurationType = TwistedInterfaceConfiguration + _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 + # substrate = configuration.substrate max_search = self.build_parameters.max_search - a1 = film.lattice.vector_arrays[0][:2] - a2 = film.lattice.vector_arrays[1][:2] - commensurate_lattices = self.__generate_commensurate_lattices(a1, a2, max_search, configuration.twist_angle) + a = film.lattice.vector_arrays[0][:2] + b = film.lattice.vector_arrays[1][:2] + commensurate_lattices = self.__generate_commensurate_lattices(a, b, max_search, configuration.twist_angle) commensurate_lattice_pairs = [ CommensurateLatticePair(configuration=configuration, **lattice) for lattice in commensurate_lattices ] @@ -393,9 +393,7 @@ def __generate_commensurate_lattices( / (np.linalg.norm(product2[0]) * np.linalg.norm(product2[1])) ) if np.isclose(angle1, angle2, atol=0.01): - print( - f"Found commensurate lattice with angle {angle} and size metric {size_metric} !!!!!!!" - ) + print(f"Found commensurate lattice with angle {angle} and size metric {size_metric}") solutions.append( {"matrix1": matrix1, "matrix2": matrix2, "angle": angle, "size_metric": size_metric} ) @@ -406,7 +404,9 @@ def __generate_commensurate_lattices( return solutions def _post_process( - self, items: List[_GeneratedItemType], post_process_parameters: Optional[_PostProcessParametersType] + self, + items: List[_GeneratedItemType], + post_process_parameters: Optional[BaseBuilder._PostProcessParametersType] = None, ) -> List[Material]: interfaces = [] for item in items: From e40d87298b6c9a3b206c1dfb46a872817eef036d Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 20 Sep 2024 20:23:32 -0700 Subject: [PATCH 06/23] update: add check for pure rotation --- .../made/tools/build/interface/builders.py | 36 +++++-------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index a02784bf..1b9a029a 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -344,6 +344,9 @@ def __solve_angle_from_rotation_matrix(matrix, zero_tolerance=1e-6, round_digits raise ValueError("Matrix must be orthogonal (determinant = 1)") if not np.all(np.abs(matrix) <= 1): raise ValueError("Matrix have all elements less than 1") + # 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): + raise ValueError("Matrix must be a pure rotation (no scaling or shearing)") cos_theta = matrix[0, 0] sin_theta = matrix[1, 0] angle_rad = np.arctan2(sin_theta, cos_theta) @@ -372,33 +375,12 @@ def __generate_commensurate_lattices( size_metric = np.linalg.det(matrix_a1a2_inverse @ matrix1 @ matrix_a1a2) if np.abs(angle - target_angle) < self.build_parameters.angle_tolerance: - # We need to check if lattices of two materials will be the same by norm and angle - product1 = matrix1 @ matrix_a1a2 - product2 = matrix2 @ matrix_a1a2 - - norm_a1 = np.linalg.norm(product1[0]) - norm_b1 = np.linalg.norm(product1[1]) - norm_a2 = np.linalg.norm(product2[0]) - norm_b2 = np.linalg.norm(product2[1]) - - if np.isclose(norm_a1, norm_a2, atol=0.01) and np.isclose(norm_b1, norm_b2, atol=0.01): - print(matrix1, matrix2) - # let's check angle between a1 and b1 and a2 and b2, they should be the same - angle1 = np.arccos( - np.dot(product1[0], product1[1]) - / (np.linalg.norm(product1[0]) * np.linalg.norm(product1[1])) - ) - angle2 = np.arccos( - np.dot(product2[0], product2[1]) - / (np.linalg.norm(product2[0]) * np.linalg.norm(product2[1])) - ) - if np.isclose(angle1, angle2, atol=0.01): - print(f"Found commensurate lattice with angle {angle} and size metric {size_metric}") - solutions.append( - {"matrix1": matrix1, "matrix2": matrix2, "angle": angle, "size_metric": size_metric} - ) - if self.build_parameters.return_first_match: - return solutions + print(f"Found commensurate lattice with angle {angle} and size metric {size_metric}") + solutions.append( + {"matrix1": matrix1, "matrix2": matrix2, "angle": angle, "size_metric": size_metric} + ) + if self.build_parameters.return_first_match: + return solutions except ValueError: continue return solutions From 405d1fad9d6d9b0afb1637de935e0fe5b11483b5 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 22 Sep 2024 18:33:56 -0700 Subject: [PATCH 07/23] update: move mateices func to utils --- .../made/tools/build/interface/builders.py | 18 ++------------- src/py/mat3ra/made/utils.py | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index 1b9a029a..982362c2 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -3,6 +3,7 @@ import numpy as np from mat3ra.made.tools.build.supercell import create_supercell +from ....utils import create_2d_supercell_matrices from ..utils import merge_materials from ...modify import ( translate_to_z_level, @@ -321,21 +322,6 @@ def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemTyp ] return commensurate_lattice_pairs - @staticmethod - def __create_matrices(max_search: int): - 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): - # Non-zero area constraint - matrix = np.array([[s11, s12], [s21, s22]]) - determinant = np.linalg.det(matrix) - if determinant == 0 or determinant < 0: - continue - matrices.append(matrix) - return matrices - @staticmethod def __solve_angle_from_rotation_matrix(matrix, zero_tolerance=1e-6, round_digits=3): if matrix.shape != (2, 2): @@ -360,7 +346,7 @@ def __generate_commensurate_lattices( """ Generate all commensurate lattices for a given search range. """ - matrices = self.__create_matrices(max_search) + matrices = create_2d_supercell_matrices(max_search) matrix_a1a2 = np.array([a1, a2]) matrix_a1a2_inverse = np.linalg.inv(matrix_a1a2) diff --git a/src/py/mat3ra/made/utils.py b/src/py/mat3ra/made/utils.py index f9a548d4..52d258cd 100644 --- a/src/py/mat3ra/made/utils.py +++ b/src/py/mat3ra/made/utils.py @@ -77,6 +77,28 @@ 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. + 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): + # Non-zero area constraint + matrix = np.array([[s11, s12], [s21, s22]]) + determinant = np.linalg.det(matrix) + if determinant == 0 or determinant < 0: + continue + matrices.append(matrix) + return matrices + + class ValueWithId(RoundNumericValuesMixin, BaseModel): id: int = 0 value: Any = None From e0a4f00fba5879d1ea25f234db05cc2f178538a8 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 22 Sep 2024 18:38:24 -0700 Subject: [PATCH 08/23] updaete: move matrix -> angle func to utils --- .../made/tools/build/interface/builders.py | 26 +++-------------- src/py/mat3ra/made/utils.py | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index 982362c2..d28bfd64 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -2,9 +2,9 @@ import numpy as np from mat3ra.made.tools.build.supercell import create_supercell +from mat3ra.made.material import Material -from ....utils import create_2d_supercell_matrices -from ..utils import merge_materials +from ....utils import create_2d_supercell_matrices, get_angle_from_rotation_matrix from ...modify import ( translate_to_z_level, rotate_material, @@ -18,9 +18,9 @@ ZSLGenerator, ) +from ..utils import merge_materials 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 @@ -322,24 +322,6 @@ def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemTyp ] return commensurate_lattice_pairs - @staticmethod - def __solve_angle_from_rotation_matrix(matrix, zero_tolerance=1e-6, round_digits=3): - if matrix.shape != (2, 2): - raise ValueError("Input matrix must be 2x2") - if np.abs(np.linalg.det(matrix) - 1) > zero_tolerance: - raise ValueError("Matrix must be orthogonal (determinant = 1)") - if not np.all(np.abs(matrix) <= 1): - raise ValueError("Matrix have all elements less than 1") - # 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): - raise ValueError("Matrix must be a pure rotation (no scaling or shearing)") - 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 - def __generate_commensurate_lattices( self, a1: List[float], a2: List[float], max_search: int = 10, target_angle: float = 0.0 ): @@ -357,7 +339,7 @@ def __generate_commensurate_lattices( intermediate_product = matrix2_inverse @ matrix1 product = matrix_a1a2_inverse @ intermediate_product @ matrix_a1a2 try: - angle = self.__solve_angle_from_rotation_matrix(product) + angle = get_angle_from_rotation_matrix(product) size_metric = np.linalg.det(matrix_a1a2_inverse @ matrix1 @ matrix_a1a2) if np.abs(angle - target_angle) < self.build_parameters.angle_tolerance: diff --git a/src/py/mat3ra/made/utils.py b/src/py/mat3ra/made/utils.py index 52d258cd..02f1752f 100644 --- a/src/py/mat3ra/made/utils.py +++ b/src/py/mat3ra/made/utils.py @@ -99,6 +99,34 @@ def create_2d_supercell_matrices(max_search: int) -> List[np.ndarray]: return matrices +def get_angle_from_rotation_matrix(matrix: np.ndarray, zero_tolerance: float = 1e-6, round_digits: int = 3): + """ + 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: + float: The angle in degrees. + """ + if matrix.shape != (2, 2): + raise ValueError("Input matrix must be 2x2") + if np.abs(np.linalg.det(matrix) - 1) > zero_tolerance: + raise ValueError("Matrix must be orthogonal (determinant = 1)") + if not np.all(np.abs(matrix) <= 1): + raise ValueError("Matrix have all elements less than 1") + # 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): + raise ValueError("Matrix must be a pure rotation (no scaling or shearing)") + 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 From f563b20b737859e5d309b5d4647d208470492041 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 22 Sep 2024 18:40:57 -0700 Subject: [PATCH 09/23] chore: cleanup imports --- .../made/tools/build/interface/builders.py | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index d28bfd64..7f812d14 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -1,9 +1,14 @@ from typing import Any, List, Optional import numpy as np -from mat3ra.made.tools.build.supercell import create_supercell -from mat3ra.made.material import Material +from pydantic import BaseModel, Field +from ase.build.tools import niggli_reduce +from pymatgen.analysis.interfaces.coherent_interfaces import ( + CoherentInterfaceBuilder, + ZSLGenerator, +) +from mat3ra.made.material import Material from ....utils import create_2d_supercell_matrices, get_angle_from_rotation_matrix from ...modify import ( translate_to_z_level, @@ -11,29 +16,22 @@ translate_by_vector, add_vacuum_sides, ) -from pydantic import BaseModel, Field -from ase.build.tools import niggli_reduce -from pymatgen.analysis.interfaces.coherent_interfaces import ( - CoherentInterfaceBuilder, - ZSLGenerator, -) - -from ..utils import merge_materials +from ...analyze import get_chemical_formula +from ...convert import to_ase, from_ase, to_pymatgen, PymatgenInterface, ASEAtoms +from ...build import BaseBuilder, BaseConfiguration 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 .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 ..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 class InterfaceBuilderParameters(BaseModel): From 693d94687ca7e6c0b3b47431d5e7baabf2b621d6 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 22 Sep 2024 18:46:21 -0700 Subject: [PATCH 10/23] update: move try loop into func --- .../made/tools/build/interface/builders.py | 6 +-- src/py/mat3ra/made/utils.py | 39 +++++++++++-------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index 7f812d14..eae00154 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -336,8 +336,8 @@ def __generate_commensurate_lattices( matrix2_inverse = np.linalg.inv(matrix2) intermediate_product = matrix2_inverse @ matrix1 product = matrix_a1a2_inverse @ intermediate_product @ matrix_a1a2 - try: - angle = get_angle_from_rotation_matrix(product) + angle = get_angle_from_rotation_matrix(product) + if angle is not None: size_metric = np.linalg.det(matrix_a1a2_inverse @ matrix1 @ matrix_a1a2) if np.abs(angle - target_angle) < self.build_parameters.angle_tolerance: @@ -347,7 +347,7 @@ def __generate_commensurate_lattices( ) if self.build_parameters.return_first_match: return solutions - except ValueError: + else: continue return solutions diff --git a/src/py/mat3ra/made/utils.py b/src/py/mat3ra/made/utils.py index 02f1752f..f968d0b8 100644 --- a/src/py/mat3ra/made/utils.py +++ b/src/py/mat3ra/made/utils.py @@ -99,7 +99,9 @@ def create_2d_supercell_matrices(max_search: int) -> List[np.ndarray]: return matrices -def get_angle_from_rotation_matrix(matrix: np.ndarray, zero_tolerance: float = 1e-6, round_digits: int = 3): +def get_angle_from_rotation_matrix( + 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: @@ -108,23 +110,26 @@ def get_angle_from_rotation_matrix(matrix: np.ndarray, zero_tolerance: float = 1 round_digits: The number of digits to round the angle. Returns: - float: The angle in degrees. + Union[float, None]: The angle in degrees if it's a pure rotation matrix, otherwise None. """ - if matrix.shape != (2, 2): - raise ValueError("Input matrix must be 2x2") - if np.abs(np.linalg.det(matrix) - 1) > zero_tolerance: - raise ValueError("Matrix must be orthogonal (determinant = 1)") - if not np.all(np.abs(matrix) <= 1): - raise ValueError("Matrix have all elements less than 1") - # 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): - raise ValueError("Matrix must be a pure rotation (no scaling or shearing)") - 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 + try: + if matrix.shape != (2, 2): + raise ValueError("Input matrix must be 2x2") + if np.abs(np.linalg.det(matrix) - 1) > zero_tolerance: + raise ValueError("Matrix must be orthogonal (determinant = 1)") + if not np.all(np.abs(matrix) <= 1): + raise ValueError("Matrix have all elements less than 1") + # 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): + raise ValueError("Matrix must be a pure rotation (no scaling or shearing)") + 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 + except ValueError as e: + print(f"Error: {e}") + return None class ValueWithId(RoundNumericValuesMixin, BaseModel): From 71bcb7a591a95019af65199d650bbe133fa92fa8 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 22 Sep 2024 18:53:25 -0700 Subject: [PATCH 11/23] chore: cleanup commensurate lattices --- .../made/tools/build/interface/builders.py | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index eae00154..3fa706b5 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -314,36 +314,56 @@ def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemTyp max_search = self.build_parameters.max_search a = film.lattice.vector_arrays[0][:2] b = film.lattice.vector_arrays[1][:2] - commensurate_lattices = self.__generate_commensurate_lattices(a, b, max_search, configuration.twist_angle) - commensurate_lattice_pairs = [ - CommensurateLatticePair(configuration=configuration, **lattice) for lattice in commensurate_lattices - ] + commensurate_lattice_pairs = self.__generate_commensurate_lattices( + configuration, a, b, max_search, configuration.twist_angle + ) return commensurate_lattice_pairs def __generate_commensurate_lattices( - self, a1: List[float], a2: List[float], max_search: int = 10, target_angle: float = 0.0 - ): + 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. + 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_a1a2 = np.array([a1, a2]) - matrix_a1a2_inverse = np.linalg.inv(matrix_a1a2) + matrix_ab = np.array([a, b]) + matrix_ab_inverse = np.linalg.inv(matrix_ab) - solutions = [] + 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_a1a2_inverse @ intermediate_product @ matrix_a1a2 + product = matrix_ab_inverse @ intermediate_product @ matrix_ab angle = get_angle_from_rotation_matrix(product) if angle is not None: - size_metric = np.linalg.det(matrix_a1a2_inverse @ matrix1 @ matrix_a1a2) + 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( - {"matrix1": matrix1, "matrix2": matrix2, "angle": angle, "size_metric": size_metric} + CommensurateLatticePair( + configuration=configuration, + matrix1=matrix1, + matrix2=matrix2, + angle=angle, + size_metric=size_metric, + ) ) if self.build_parameters.return_first_match: return solutions From 4ece86b5e2b4501ace6843f0e392b935d3ed99e9 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 22 Sep 2024 19:00:41 -0700 Subject: [PATCH 12/23] chore: type fix --- src/py/mat3ra/made/tools/build/interface/builders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index 3fa706b5..024d3bcd 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -374,7 +374,7 @@ def __generate_commensurate_lattices( def _post_process( self, items: List[_GeneratedItemType], - post_process_parameters: Optional[BaseBuilder._PostProcessParametersType] = None, + post_process_parameters=None, ) -> List[Material]: interfaces = [] for item in items: From e41c2558f6b20ada95b24a93be98eb9564e4d3fd Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 22 Sep 2024 19:05:30 -0700 Subject: [PATCH 13/23] update: move configs to configs --- .../tools/build/interface/configuration.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/py/mat3ra/made/tools/build/interface/configuration.py b/src/py/mat3ra/made/tools/build/interface/configuration.py index 2448cae7..8d29f827 100644 --- a/src/py/mat3ra/made/tools/build/interface/configuration.py +++ b/src/py/mat3ra/made/tools/build/interface/configuration.py @@ -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 @@ -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, + } From cf8b0f598e2eceb12b13909df2159a8c0ab08135 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 22 Sep 2024 19:05:51 -0700 Subject: [PATCH 14/23] update: create commlat pair file --- .../build/interface/commensurate_lattice_pair.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/py/mat3ra/made/tools/build/interface/commensurate_lattice_pair.py diff --git a/src/py/mat3ra/made/tools/build/interface/commensurate_lattice_pair.py b/src/py/mat3ra/made/tools/build/interface/commensurate_lattice_pair.py new file mode 100644 index 00000000..c1d4fd21 --- /dev/null +++ b/src/py/mat3ra/made/tools/build/interface/commensurate_lattice_pair.py @@ -0,0 +1,15 @@ +import numpy as np +from pydantic import BaseModel + +from .configuration import TwistedInterfaceConfiguration + + +class CommensurateLatticePair(BaseModel): + class Config: + arbitrary_types_allowed = True + + configuration: TwistedInterfaceConfiguration + matrix1: np.ndarray + matrix2: np.ndarray + angle: float + size_metric: float From 01fd9271eef83a98d51cc703a8fc307e6f669f0a Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 22 Sep 2024 19:10:01 -0700 Subject: [PATCH 15/23] use new structures + types --- .../made/tools/build/interface/builders.py | 75 +++---------------- 1 file changed, 12 insertions(+), 63 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index 024d3bcd..8242fe3c 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -1,7 +1,7 @@ from typing import Any, List, Optional import numpy as np -from pydantic import BaseModel, Field +from pydantic import BaseModel from ase.build.tools import niggli_reduce from pymatgen.analysis.interfaces.coherent_interfaces import ( CoherentInterfaceBuilder, @@ -18,7 +18,7 @@ ) from ...analyze import get_chemical_formula from ...convert import to_ase, from_ase, to_pymatgen, PymatgenInterface, ASEAtoms -from ...build import BaseBuilder, BaseConfiguration +from ...build import BaseBuilder from ..nanoribbon import NanoribbonConfiguration, create_nanoribbon from ..supercell import create_supercell from ..slab import create_slab, Termination, SlabConfiguration @@ -29,7 +29,12 @@ ) from .enums import StrainModes -from .configuration import InterfaceConfiguration +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 @@ -203,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, @@ -287,17 +247,6 @@ def _update_material_name( return material -class CommensurateLatticePair(BaseModel): - class Config: - arbitrary_types_allowed = True - - configuration: TwistedInterfaceConfiguration - matrix1: np.ndarray - matrix2: np.ndarray - angle: float - size_metric: float - - class CommensurateLatticeInterfaceBuilderParameters(BaseModel): max_search: int = 10 angle_tolerance: float = 0.1 From b7650e7616b30629fd7e66c4fdb51a1b6fe0d9e7 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 22 Sep 2024 19:25:42 -0700 Subject: [PATCH 16/23] update: fix failed branching logic --- src/py/mat3ra/made/utils.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/py/mat3ra/made/utils.py b/src/py/mat3ra/made/utils.py index f968d0b8..bb09ac65 100644 --- a/src/py/mat3ra/made/utils.py +++ b/src/py/mat3ra/made/utils.py @@ -112,24 +112,20 @@ def get_angle_from_rotation_matrix( Returns: Union[float, None]: The angle in degrees if it's a pure rotation matrix, otherwise None. """ - try: - if matrix.shape != (2, 2): - raise ValueError("Input matrix must be 2x2") - if np.abs(np.linalg.det(matrix) - 1) > zero_tolerance: - raise ValueError("Matrix must be orthogonal (determinant = 1)") - if not np.all(np.abs(matrix) <= 1): - raise ValueError("Matrix have all elements less than 1") - # 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): - raise ValueError("Matrix must be a pure rotation (no scaling or shearing)") - 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 - except ValueError as e: - print(f"Error: {e}") + 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): From da3a7e0360dc3bddd3c84668315101e31c4104a9 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 22 Sep 2024 19:29:41 -0700 Subject: [PATCH 17/23] update: add test verified in nb --- tests/py/unit/test_tools_build_interface.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/py/unit/test_tools_build_interface.py b/tests/py/unit/test_tools_build_interface.py index 6cdabf81..c222f154 100644 --- a/tests/py/unit/test_tools_build_interface.py +++ b/tests/py/unit/test_tools_build_interface.py @@ -9,8 +9,11 @@ create_interfaces, ) from mat3ra.made.tools.build.interface.builders import ( + CommensurateLatticeInterfaceBuilder, + CommensurateLatticeInterfaceBuilderParameters, NanoRibbonTwistedInterfaceBuilder, NanoRibbonTwistedInterfaceConfiguration, + TwistedInterfaceConfiguration, ) from mat3ra.utils import assertion as assertion_utils @@ -63,3 +66,18 @@ def test_create_twisted_nanoribbon_interface(): expected_coordinate = [0.704207885, 0.522108183, 0.65] assertion_utils.assert_deep_almost_equal(exected_cell_vectors, interface.basis.cell.vectors_as_array) assertion_utils.assert_deep_almost_equal(expected_coordinate, interface.basis.coordinates.values[42]) + + +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(max_search=5, angle_tolerance=0.5, return_first_match=True) + builder = CommensurateLatticeInterfaceBuilder(build_parameters=params) + interfaces = builder.get_materials(config, post_process_parameters=config) + assert len(interfaces) == 1 + interface = interfaces[0] + print(interface.basis.cell.vectors_as_array) + expected_cell_vectors = [[10.754672133, 0.0, 0.0], [5.377336066500001, 9.313819276550575, 0.0], [0.0, 0.0, 20.0]] + + assertion_utils.assert_deep_almost_equal(expected_cell_vectors, interface.basis.cell.vectors_as_array) From 4383c7657688b21560c284db25ddfaac3450c837 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:17:18 -0700 Subject: [PATCH 18/23] chore: fix test --- tests/py/unit/test_tools_build_interface.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/py/unit/test_tools_build_interface.py b/tests/py/unit/test_tools_build_interface.py index c222f154..2bde6a76 100644 --- a/tests/py/unit/test_tools_build_interface.py +++ b/tests/py/unit/test_tools_build_interface.py @@ -77,7 +77,6 @@ def test_create_commensurate_supercell_twisted_interface(): interfaces = builder.get_materials(config, post_process_parameters=config) assert len(interfaces) == 1 interface = interfaces[0] - print(interface.basis.cell.vectors_as_array) - expected_cell_vectors = [[10.754672133, 0.0, 0.0], [5.377336066500001, 9.313819276550575, 0.0], [0.0, 0.0, 20.0]] + expected_cell_vectors = [[-9.869164, -4.273473, 0.0], [-1.233646, -10.683683, 0.0], [0.0, 0.0, 20.0]] assertion_utils.assert_deep_almost_equal(expected_cell_vectors, interface.basis.cell.vectors_as_array) From 30f5a4c4acc00be90ff6788b924d81648ba2ab87 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:03:53 -0700 Subject: [PATCH 19/23] chore: cleanups --- .../made/tools/build/interface/builders.py | 20 ++++++++++++------- src/py/mat3ra/made/utils.py | 6 +++++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index 8242fe3c..7f0a0a69 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -248,7 +248,16 @@ def _update_material_name( class CommensurateLatticeInterfaceBuilderParameters(BaseModel): - max_search: int = 10 + """ + 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 @@ -260,7 +269,7 @@ class CommensurateLatticeInterfaceBuilder(BaseBuilder): def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]: film = configuration.film # substrate = configuration.substrate - max_search = self.build_parameters.max_search + max_search = self.build_parameters.max_repetition_int a = film.lattice.vector_arrays[0][:2] b = film.lattice.vector_arrays[1][:2] commensurate_lattice_pairs = self.__generate_commensurate_lattices( @@ -332,9 +341,6 @@ def _post_process( new_film = translate_by_vector( new_film, [0, 0, item.configuration.distance_z], use_cartesian_coordinates=True ) - try: - interface = merge_materials([new_substrate, new_film]) - interfaces.append(interface) - except Exception as e: - print(e) + interface = merge_materials([new_substrate, new_film]) + interfaces.append(interface) return interfaces diff --git a/src/py/mat3ra/made/utils.py b/src/py/mat3ra/made/utils.py index bb09ac65..ca3f93db 100644 --- a/src/py/mat3ra/made/utils.py +++ b/src/py/mat3ra/made/utils.py @@ -80,6 +80,11 @@ def get_overlapping_coordinates( 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: @@ -90,7 +95,6 @@ def create_2d_supercell_matrices(max_search: int) -> List[np.ndarray]: 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): - # Non-zero area constraint matrix = np.array([[s11, s12], [s21, s22]]) determinant = np.linalg.det(matrix) if determinant == 0 or determinant < 0: From 5ff26a94854f9b30493f2b800330ea2250c041ce Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:17:49 -0700 Subject: [PATCH 20/23] chore: cleanups 2 --- src/py/mat3ra/made/tools/build/interface/builders.py | 4 ++-- .../build/interface/commensurate_lattice_pair.py | 11 +++++++++++ src/py/mat3ra/made/utils.py | 2 +- tests/py/unit/test_tools_build_interface.py | 4 +++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index 7f0a0a69..27bdb70f 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -9,7 +9,7 @@ ) from mat3ra.made.material import Material -from ....utils import create_2d_supercell_matrices, get_angle_from_rotation_matrix +from ....utils import create_2d_supercell_matrices, get_angle_from_rotation_matrix_2d from ...modify import ( translate_to_z_level, rotate_material, @@ -308,7 +308,7 @@ def __generate_commensurate_lattices( 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(product) + angle = get_angle_from_rotation_matrix_2d(product) if angle is not None: size_metric = np.linalg.det(matrix_ab_inverse @ matrix1 @ matrix_ab) diff --git a/src/py/mat3ra/made/tools/build/interface/commensurate_lattice_pair.py b/src/py/mat3ra/made/tools/build/interface/commensurate_lattice_pair.py index c1d4fd21..a341c287 100644 --- a/src/py/mat3ra/made/tools/build/interface/commensurate_lattice_pair.py +++ b/src/py/mat3ra/made/tools/build/interface/commensurate_lattice_pair.py @@ -5,6 +5,17 @@ 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. + 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: arbitrary_types_allowed = True diff --git a/src/py/mat3ra/made/utils.py b/src/py/mat3ra/made/utils.py index ca3f93db..9f158a5c 100644 --- a/src/py/mat3ra/made/utils.py +++ b/src/py/mat3ra/made/utils.py @@ -103,7 +103,7 @@ def create_2d_supercell_matrices(max_search: int) -> List[np.ndarray]: return matrices -def get_angle_from_rotation_matrix( +def get_angle_from_rotation_matrix_2d( matrix: np.ndarray, zero_tolerance: float = 1e-6, round_digits: int = 3 ) -> Union[float, None]: """ diff --git a/tests/py/unit/test_tools_build_interface.py b/tests/py/unit/test_tools_build_interface.py index 2bde6a76..94c70371 100644 --- a/tests/py/unit/test_tools_build_interface.py +++ b/tests/py/unit/test_tools_build_interface.py @@ -72,7 +72,9 @@ 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(max_search=5, angle_tolerance=0.5, return_first_match=True) + params = CommensurateLatticeInterfaceBuilderParameters( + max_repetition_int=5, angle_tolerance=0.5, return_first_match=True + ) builder = CommensurateLatticeInterfaceBuilder(build_parameters=params) interfaces = builder.get_materials(config, post_process_parameters=config) assert len(interfaces) == 1 From f29463a3f699512dbfc5a9eed5a3eecbd75776c0 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:35:30 -0700 Subject: [PATCH 21/23] update: add actual angle metadata + test that --- src/py/mat3ra/made/tools/build/interface/builders.py | 9 +++++++++ tests/py/unit/test_tools_build_interface.py | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index 27bdb70f..ccb5627e 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -342,5 +342,14 @@ def _post_process( 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 diff --git a/tests/py/unit/test_tools_build_interface.py b/tests/py/unit/test_tools_build_interface.py index 94c70371..f2680240 100644 --- a/tests/py/unit/test_tools_build_interface.py +++ b/tests/py/unit/test_tools_build_interface.py @@ -80,5 +80,6 @@ def test_create_commensurate_supercell_twisted_interface(): assert len(interfaces) == 1 interface = interfaces[0] expected_cell_vectors = [[-9.869164, -4.273473, 0.0], [-1.233646, -10.683683, 0.0], [0.0, 0.0, 20.0]] - assertion_utils.assert_deep_almost_equal(expected_cell_vectors, interface.basis.cell.vectors_as_array) + expected_angle = 13.174 + assert interface.metadata["build"]["configuration"]["actual_twist_angle"] == expected_angle From 7f803d213f93e5d1a49c04a48e45094705ad7de4 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 23 Sep 2024 19:23:29 -0700 Subject: [PATCH 22/23] chore: adjust description --- .../made/tools/build/interface/commensurate_lattice_pair.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/interface/commensurate_lattice_pair.py b/src/py/mat3ra/made/tools/build/interface/commensurate_lattice_pair.py index a341c287..74c8ab7b 100644 --- a/src/py/mat3ra/made/tools/build/interface/commensurate_lattice_pair.py +++ b/src/py/mat3ra/made/tools/build/interface/commensurate_lattice_pair.py @@ -10,8 +10,8 @@ class CommensurateLatticePair(BaseModel): Attributes: configuration (TwistedInterfaceConfiguration): The configuration of the twisted interface. - matrix1 (np.ndarray): The supercell matrix for the first lattice. - matrix2 (np.ndarray): The supercell matrix for the second lattice. + matrix1 (np.ndarray): The supercell 2D matrix for the first lattice. + matrix2 (np.ndarray): The supercell 2D 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. """ From 71b98a39005ff20e407ea0ce94f1024f87bb015b Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 23 Sep 2024 19:29:07 -0700 Subject: [PATCH 23/23] chore: add angle units --- src/py/mat3ra/made/tools/build/interface/builders.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index ccb5627e..d2e7f0da 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -253,7 +253,8 @@ class CommensurateLatticeInterfaceBuilderParameters(BaseModel): 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. + angle_tolerance (float): The tolerance for the angle between the commensurate lattices + and the target angle, in degrees. return_first_match (bool): Whether to return the first match or all matches. """ @@ -293,7 +294,7 @@ def __generate_commensurate_lattices( 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. + target_angle (float): The target angle, in degrees. Returns: List[CommensurateLatticePair]: The list of commensurate lattice pairs