diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index f544b581..2c72c1c6 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -15,6 +15,7 @@ rotate, translate_by_vector, add_vacuum_sides, + add_vacuum, ) from ...analyze import get_chemical_formula from ...convert import to_ase, from_ase, to_pymatgen, PymatgenInterface, ASEAtoms @@ -28,7 +29,7 @@ ConvertGeneratedItemsPymatgenStructureMixin, ) -from .enums import StrainModes +from .enums import StrainModes, angle_to_supercell_matrix_values_for_hex from .configuration import ( InterfaceConfiguration, NanoRibbonTwistedInterfaceConfiguration, @@ -243,7 +244,7 @@ def _generate(self, configuration: _ConfigurationType) -> List[Material]: def _update_material_name( self, material: Material, configuration: NanoRibbonTwistedInterfaceConfiguration ) -> Material: - material.name = f"Twisted Nanoribbon Interface ({configuration.twist_angle:.2f}°)" + material.name = f"Twisted Nanoribbon Interface ({configuration.twist_angle:.2f} degrees)" return material @@ -252,13 +253,16 @@ class CommensurateLatticeTwistedInterfaceBuilderParameters(BaseModel): Parameters for the commensurate lattice interface builder. Args: - max_repetition_int (int): The maximum search range for commensurate lattices. + max_supercell_matrix_int (Optional[int]): The maximum integer for the transformation matrices. + If not provided, it will be determined based on the target angle and the lattice vectors automatically. + limit_max_int (Optional[int]): The limit for the maximum integer for the transformation matrices when searching 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. """ - max_repetition_int: int = 10 + max_supercell_matrix_int: Optional[int] = None + limit_max_int: Optional[int] = 42 angle_tolerance: float = 0.1 return_first_match: bool = False @@ -270,36 +274,65 @@ class CommensurateLatticeTwistedInterfaceBuilder(BaseBuilder): 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] - commensurate_lattice_pairs = self.__generate_commensurate_lattices( - configuration, a, b, max_search, configuration.twist_angle + max_int = self.build_parameters.max_supercell_matrix_int or self.__get_initial_guess_for_max_int( + film, configuration.twist_angle ) + commensurate_lattice_pairs: List[CommensurateLatticePair] = [] + while not commensurate_lattice_pairs and max_int < self.build_parameters.limit_max_int: + print(f"Trying max_int = {max_int}") + commensurate_lattice_pairs = self.__generate_commensurate_lattices( + configuration, a, b, max_int, configuration.twist_angle + ) + max_int += 1 + return commensurate_lattice_pairs + def __get_initial_guess_for_max_int(self, film, target_angle: float) -> int: + """ + Determine the maximum integer for the transformation matrices based on the target angle. + + Args: + a (List[float]): The a lattice vector. + b (List[float]): The b lattice vector. + target_angle (float): The target twist angle, in degrees. + + Returns: + int: The maximum integer for the transformation matrices. + """ + if film.lattice.type == "HEX": + # getting max int of the matrix that has angle closest to target angle + xy_supercell_matrix_for_closest_angle = min( + angle_to_supercell_matrix_values_for_hex, key=lambda x: abs(x["angle"] - target_angle) + ) + # Get maximum absolute value from the supercell matrix values + return max(abs(x) for row in xy_supercell_matrix_for_closest_angle["xy_supercell"] for x in row) + return 1 + def __generate_commensurate_lattices( self, configuration: TwistedInterfaceConfiguration, a: List[float], b: List[float], - max_search: int = 10, + max_supercell_matrix_element_int: int, target_angle: float = 0.0, ) -> List[CommensurateLatticePair]: """ - Generate all commensurate lattices for a given search range and filter by closeness to target angle. + Generate all commensurate lattices for a given target angle 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, in degrees. + max_supercell_matrix_element_int (int): The maximum integer for the transformation matrices. + target_angle (float): The target twist angle, in degrees. Returns: List[CommensurateLatticePair]: The list of commensurate lattice pairs """ - matrices = create_2d_supercell_matrices(max_search) + # Generate the supercell matrices using the calculated max_supercell_matrix_element_int + matrices = create_2d_supercell_matrices(max_supercell_matrix_element_int) matrix_ab = np.array([a, b]) matrix_ab_inverse = np.linalg.inv(matrix_ab) @@ -342,8 +375,10 @@ def _post_process( 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 = merge_materials([new_substrate, new_film], merge_dangerously=True) interface.metadata["actual_twist_angle"] = item.angle + if item.configuration.vacuum != 0: + interface = add_vacuum(interface, item.configuration.vacuum) interfaces.append(interface) return interfaces @@ -354,3 +389,9 @@ def _update_material_metadata(self, material, configuration) -> Material: "actual_twist_angle" ] return updated_material + + def _update_material_name( + self, material: Material, configuration: NanoRibbonTwistedInterfaceConfiguration + ) -> Material: + material.name = f"Twisted Bilayer Interface ({material.metadata['actual_twist_angle']:.2f} degrees)" + return material diff --git a/src/py/mat3ra/made/tools/build/interface/configuration.py b/src/py/mat3ra/made/tools/build/interface/configuration.py index 5269c7aa..9f863566 100644 --- a/src/py/mat3ra/made/tools/build/interface/configuration.py +++ b/src/py/mat3ra/made/tools/build/interface/configuration.py @@ -40,6 +40,7 @@ class TwistedInterfaceConfiguration(BaseConfiguration): substrate: Optional[Material] = None twist_angle: float = 0.0 distance_z: float = 3.0 + vacuum: float = 0.0 @property def _json(self): diff --git a/src/py/mat3ra/made/tools/build/interface/enums.py b/src/py/mat3ra/made/tools/build/interface/enums.py index 364d19b2..7a776006 100644 --- a/src/py/mat3ra/made/tools/build/interface/enums.py +++ b/src/py/mat3ra/made/tools/build/interface/enums.py @@ -1,4 +1,5 @@ from enum import Enum +from typing import TypedDict, List class StrainModes(str, Enum): @@ -10,3 +11,56 @@ class StrainModes(str, Enum): class SupercellTypes(str, Enum): hexagonal = "hexagonal" orthogonal = "orthogonal" + + +class SupercellMatrix(TypedDict): + angle: float + xy_supercell: List[List[int]] + + +# Tabulation from https://github.com/qtm-iisc/Twister/blob/474156a2a59f2b9d59350b32de56864a9496f848/examples/Homobilayer_hex/hex.table +# Maps twist angle to supercell matrix values for homo-material hexagonal supercells bilayer +angle_to_supercell_matrix_values_for_hex: List[SupercellMatrix] = [ + {"angle": 60.0, "xy_supercell": [[0, 1], [-1, 1]]}, + {"angle": 21.7867892983, "xy_supercell": [[1, 2], [-2, 3]]}, + {"angle": 13.1735511073, "xy_supercell": [[2, 3], [-3, 5]]}, + {"angle": 9.4300079079, "xy_supercell": [[3, 4], [-4, 7]]}, + {"angle": 7.34099301663, "xy_supercell": [[4, 5], [-5, 9]]}, + {"angle": 6.00898319777, "xy_supercell": [[5, 6], [-6, 11]]}, + {"angle": 5.08584780812, "xy_supercell": [[6, 7], [-7, 13]]}, + {"angle": 4.40845500794, "xy_supercell": [[7, 8], [-8, 15]]}, + {"angle": 3.89023816901, "xy_supercell": [[8, 9], [-9, 17]]}, + {"angle": 3.48100608947, "xy_supercell": [[9, 10], [-10, 19]]}, + {"angle": 3.14965742639, "xy_supercell": [[10, 11], [-11, 21]]}, + {"angle": 2.87589463363, "xy_supercell": [[11, 12], [-12, 23]]}, + {"angle": 2.64590838119, "xy_supercell": [[12, 13], [-13, 25]]}, + {"angle": 2.44997727662, "xy_supercell": [[13, 14], [-14, 27]]}, + {"angle": 2.28105960973, "xy_supercell": [[14, 15], [-15, 29]]}, + {"angle": 2.1339296666, "xy_supercell": [[15, 16], [-16, 31]]}, + {"angle": 2.00462783069, "xy_supercell": [[16, 17], [-17, 33]]}, + {"angle": 1.89009907347, "xy_supercell": [[17, 18], [-18, 35]]}, + {"angle": 1.78794861038, "xy_supercell": [[18, 19], [-19, 37]]}, + {"angle": 1.69627269352, "xy_supercell": [[19, 20], [-20, 39]]}, + {"angle": 1.61353890116, "xy_supercell": [[20, 21], [-21, 41]]}, + {"angle": 1.53849981837, "xy_supercell": [[21, 22], [-22, 43]]}, + {"angle": 1.47012972578, "xy_supercell": [[22, 23], [-23, 45]]}, + {"angle": 1.40757744635, "xy_supercell": [[23, 24], [-24, 47]]}, + {"angle": 1.35013073557, "xy_supercell": [[24, 25], [-25, 49]]}, + {"angle": 1.29718904759, "xy_supercell": [[25, 26], [-26, 51]]}, + {"angle": 1.24824246553, "xy_supercell": [[26, 27], [-27, 53]]}, + {"angle": 1.20285522748, "xy_supercell": [[27, 28], [-28, 55]]}, + {"angle": 1.16065271985, "xy_supercell": [[28, 29], [-29, 57]]}, + {"angle": 1.12131111538, "xy_supercell": [[29, 30], [-30, 59]]}, + {"angle": 1.08454904916, "xy_supercell": [[30, 31], [-31, 61]]}, + {"angle": 1.05012087979, "xy_supercell": [[31, 32], [-32, 63]]}, + {"angle": 1.01781119445, "xy_supercell": [[32, 33], [-33, 65]]}, + {"angle": 0.987430297814, "xy_supercell": [[33, 34], [-34, 67]]}, + {"angle": 0.958810485525, "xy_supercell": [[34, 35], [-35, 69]]}, + {"angle": 0.931802947264, "xy_supercell": [[35, 36], [-36, 71]]}, + {"angle": 0.906275178895, "xy_supercell": [[36, 37], [-37, 73]]}, + {"angle": 0.882108808579, "xy_supercell": [[37, 38], [-38, 75]]}, + {"angle": 0.859197761631, "xy_supercell": [[38, 39], [-39, 77]]}, + {"angle": 0.8374467041, "xy_supercell": [[39, 40], [-40, 79]]}, + {"angle": 0.816769716893, "xy_supercell": [[40, 41], [-41, 81]]}, + {"angle": 0.0, "xy_supercell": [[1, 0], [0, 1]]}, +]