diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index 94bee02d..12d7e88a 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -7,7 +7,7 @@ from .convert import decorator_convert_material_args_kwargs_to_atoms, to_pymatgen from .enums import SurfaceTypes from .third_party import ASEAtoms, PymatgenIStructure, PymatgenVoronoiNN -from .utils import decorator_handle_periodic_boundary_conditions +from .utils import decorator_convert_position_to_coordinate, decorator_handle_periodic_boundary_conditions @decorator_convert_material_args_kwargs_to_atoms @@ -471,6 +471,7 @@ def get_undercoordinated_atom_indices( return undercoordinated_atoms_indices +@decorator_convert_position_to_coordinate def get_local_extremum_atom_index( material: Material, coordinate: List[float], diff --git a/src/py/mat3ra/made/tools/build/defect/builders.py b/src/py/mat3ra/made/tools/build/defect/builders.py index 637562bf..66f6bc35 100644 --- a/src/py/mat3ra/made/tools/build/defect/builders.py +++ b/src/py/mat3ra/made/tools/build/defect/builders.py @@ -15,11 +15,11 @@ from ...modify import ( add_vacuum, - filter_material_by_ids, + filter_by_ids, filter_by_box, filter_by_condition_on_coordinates, translate_to_z_level, - rotate_material, + rotate, ) from ...build import BaseBuilder from ...convert import to_pymatgen @@ -28,6 +28,7 @@ get_atomic_coordinates_extremum, get_closest_site_id_from_coordinate, get_closest_site_id_from_coordinate_and_element, + get_local_extremum_atom_index, ) from ....utils import get_center_of_coordinates from ...utils import transform_coordinate_to_supercell, coordinate as CoordinateCondition @@ -174,7 +175,10 @@ def create_adatom( def _calculate_coordinate_from_position_and_distance( self, material: Material, position_on_surface: List[float], distance_z: float ) -> List[float]: - max_z = get_atomic_coordinates_extremum(material, use_cartesian_coordinates=False) + max_z_id = get_local_extremum_atom_index( + material, position_on_surface, "max", vicinity=3.0, use_cartesian_coordinates=False + ) + max_z = material.basis.coordinates.get_element_value_by_index(max_z_id)[2] distance_in_crystal_units = distance_z / material.lattice.c return [position_on_surface[0], position_on_surface[1], max_z + distance_in_crystal_units] @@ -290,7 +294,7 @@ def create_isolated_adatom( closest_site_id = get_closest_site_id_from_coordinate_and_element( material, approximate_adatom_coordinate_cartesian, chemical_element ) - only_adatom_material = filter_material_by_ids(material, [closest_site_id]) + only_adatom_material = filter_by_ids(material, [closest_site_id]) return only_adatom_material def create_adatom( @@ -612,7 +616,7 @@ def create_terrace( if rotate_to_match_pbc: adjusted_material = self._increase_lattice_size(result_material, delta_length, normalized_direction_vector) - result_material = rotate_material(material=adjusted_material, axis=normalized_rotation_axis, angle=angle) + result_material = rotate(material=adjusted_material, axis=normalized_rotation_axis, angle=angle) return result_material def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]: diff --git a/src/py/mat3ra/made/tools/build/interface/__init__.py b/src/py/mat3ra/made/tools/build/interface/__init__.py index 6462e771..89a08c44 100644 --- a/src/py/mat3ra/made/tools/build/interface/__init__.py +++ b/src/py/mat3ra/made/tools/build/interface/__init__.py @@ -4,7 +4,7 @@ from mat3ra.made.material import Material from ...calculate.calculators import InterfaceMaterialCalculator -from ...modify import displace_interface_part +from ...modify import interface_displace_part from ...optimize import evaluate_calculator_on_xy_grid from .builders import ( SimpleInterfaceBuilder, @@ -64,7 +64,7 @@ def get_optimal_film_displacement( xy_matrix, results_matrix = evaluate_calculator_on_xy_grid( material=material, calculator_function=calculator.get_energy, - modifier=displace_interface_part, + modifier=interface_displace_part, modifier_parameters={}, grid_size_xy=grid_size_xy, grid_offset_position=grid_offset_position, diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index f8a0e5ad..5179d361 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -12,7 +12,7 @@ from ....utils import create_2d_supercell_matrices, get_angle_from_rotation_matrix_2d from ...modify import ( translate_to_z_level, - rotate_material, + rotate, translate_by_vector, add_vacuum_sides, ) @@ -229,7 +229,7 @@ def _generate(self, configuration: _ConfigurationType) -> List[Material]: length=configuration.ribbon_length, ) top_ribbon = create_nanoribbon(top_ribbon_configuration) - top_ribbon = rotate_material(top_ribbon, [0, 0, 1], configuration.twist_angle, wrap=False) + top_ribbon = rotate(top_ribbon, [0, 0, 1], configuration.twist_angle, wrap=False) translation_vector = [0, 0, configuration.distance_z] top_ribbon = translate_by_vector(top_ribbon, translation_vector, use_cartesian_coordinates=True) diff --git a/src/py/mat3ra/made/tools/build/slab/__init__.py b/src/py/mat3ra/made/tools/build/slab/__init__.py index 25fcce7a..e93db5e1 100644 --- a/src/py/mat3ra/made/tools/build/slab/__init__.py +++ b/src/py/mat3ra/made/tools/build/slab/__init__.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from mat3ra.made.material import Material from .builders import SlabBuilder, SlabSelectorParameters @@ -10,6 +10,7 @@ def get_terminations(configuration: SlabConfiguration) -> List[Termination]: return SlabBuilder().get_terminations(configuration) -def create_slab(configuration: SlabConfiguration, termination: Termination) -> Material: +def create_slab(configuration: SlabConfiguration, termination: Optional[Termination] = None) -> Material: builder = SlabBuilder() + termination = termination or builder.get_terminations(configuration)[0] return builder.get_material(configuration, selector_parameters=SlabSelectorParameters(termination=termination)) diff --git a/src/py/mat3ra/made/tools/calculate/calculators.py b/src/py/mat3ra/made/tools/calculate/calculators.py index 963096e3..b8074976 100644 --- a/src/py/mat3ra/made/tools/calculate/calculators.py +++ b/src/py/mat3ra/made/tools/calculate/calculators.py @@ -7,7 +7,7 @@ from ..analyze import get_surface_atom_indices from ..convert.utils import InterfacePartsEnum from ..enums import SurfaceTypes -from ..modify import get_interface_part +from ..modify import interface_get_part from .interaction_functions import sum_of_inverse_distances_squared @@ -101,8 +101,8 @@ def get_energy( Returns: float: The calculated interaction energy between the film and substrate. """ - film_material = get_interface_part(material, part=InterfacePartsEnum.FILM) - substrate_material = get_interface_part(material, part=InterfacePartsEnum.SUBSTRATE) + film_material = interface_get_part(material, part=InterfacePartsEnum.FILM) + substrate_material = interface_get_part(material, part=InterfacePartsEnum.SUBSTRATE) film_surface_atom_indices = get_surface_atom_indices( film_material, SurfaceTypes.BOTTOM, shadowing_radius=shadowing_radius diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index 79762c0e..ba3dce38 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -128,7 +128,7 @@ def wrap_to_unit_cell(material: Material) -> Material: return Material(from_ase(atoms)) -def filter_material_by_ids(material: Material, ids: List[int], invert: bool = False) -> Material: +def filter_by_ids(material: Material, ids: List[int], invert: bool = False) -> Material: """ Filter out only atoms corresponding to the ids. @@ -174,7 +174,7 @@ def filter_by_condition_on_coordinates( use_cartesian_coordinates=use_cartesian_coordinates, ) - new_material = filter_material_by_ids(new_material, ids, invert=invert_selection) + new_material = filter_by_ids(new_material, ids, invert=invert_selection) return new_material @@ -234,7 +234,7 @@ def filter_by_sphere( coordinate=center_coordinate, radius=radius, ) - return filter_material_by_ids(material, ids, invert=invert) + return filter_by_ids(material, ids, invert=invert) def filter_by_circle_projection( @@ -482,7 +482,7 @@ def remove_vacuum(material: Material, from_top=True, from_bottom=True, fixed_pad return new_material -def rotate_material(material: Material, axis: List[int], angle: float, wrap: bool = True) -> Material: +def rotate(material: Material, axis: List[int], angle: float, wrap: bool = True, rotate_cell=False) -> Material: """ Rotate the material around a given axis by a specified angle. @@ -491,6 +491,7 @@ def rotate_material(material: Material, axis: List[int], angle: float, wrap: boo axis (List[int]): The axis to rotate around, expressed as [x, y, z]. angle (float): The angle of rotation in degrees. wrap (bool): Whether to wrap the material to the unit cell. + rotate_cell (bool): Whether to rotate the cell. Returns: Atoms: The rotated material. """ @@ -499,14 +500,14 @@ def rotate_material(material: Material, axis: List[int], angle: float, wrap: boo crystal_basis.to_crystal() material.basis = crystal_basis atoms = to_ase(material) - atoms.rotate(v=axis, a=angle, center="COU") + atoms.rotate(v=axis, a=angle, center="COU", rotate_cell=rotate_cell) if wrap: atoms.wrap() return Material(from_ase(atoms)) -def displace_interface_part( +def interface_displace_part( interface: Material, displacement: List[float], label: InterfacePartsEnum = InterfacePartsEnum.FILM, @@ -542,7 +543,7 @@ def displace_interface_part( return new_material -def get_interface_part( +def interface_get_part( interface: Material, part: InterfacePartsEnum = InterfacePartsEnum.FILM, ) -> Material: diff --git a/src/py/mat3ra/made/tools/utils/__init__.py b/src/py/mat3ra/made/tools/utils/__init__.py index fb4a045b..c3d67f28 100644 --- a/src/py/mat3ra/made/tools/utils/__init__.py +++ b/src/py/mat3ra/made/tools/utils/__init__.py @@ -1,5 +1,5 @@ from functools import wraps -from typing import Callable, List, Optional, Tuple +from typing import Any, Callable, List, Optional, Tuple import numpy as np from mat3ra.made.material import Material @@ -42,6 +42,35 @@ def wrapper(*args, **kwargs): return wrapper +def decorator_convert_position_to_coordinate(func: Callable) -> Callable: + """ + A decorator that converts a 2D position [x, y] to a 3D coordinate [x, y, 0.0]. + """ + + @wraps(func) + def wrapper(*args, **kwargs): + if "coordinate" in kwargs: + coordinate = kwargs["coordinate"] + else: + # Find the position of the 'coordinate' argument and get its value + coordinate_index = func.__code__.co_varnames.index("coordinate") + coordinate = args[coordinate_index] + + if len(coordinate) == 2: + coordinate = [coordinate[0], coordinate[1], 0.0] + + if "coordinate" in kwargs: + kwargs["coordinate"] = coordinate + else: + args = list(args) + args[coordinate_index] = coordinate + args = tuple(args) + + return func(*args, **kwargs) + + return wrapper + + def get_distance_between_coordinates(coordinate1: List[float], coordinate2: List[float]) -> float: """ Get the distance between two coordinates. diff --git a/tests/py/unit/fixtures.py b/tests/py/unit/fixtures.py index 07cdd71a..34883958 100644 --- a/tests/py/unit/fixtures.py +++ b/tests/py/unit/fixtures.py @@ -170,7 +170,7 @@ "bulk": SI_CONVENTIONAL_CELL, "miller_indices": (0, 0, 1), "thickness": 1, - "vacuum": 1, + "vacuum": 5.0, "xy_supercell_matrix": [[1, 0], [0, 1]], "use_conventional_cell": True, "use_orthogonal_z": True, @@ -211,69 +211,7 @@ "_id": "", "metadata": { "boundaryConditions": {"type": "pbc", "offset": 0}, - "build": { - "termination": "Si_P6/mmm_1", - "configuration": { - "type": "SlabConfiguration", - "bulk": { - "name": "Si8", - "basis": { - "elements": [ - {"id": 0, "value": "Si"}, - {"id": 1, "value": "Si"}, - {"id": 2, "value": "Si"}, - {"id": 3, "value": "Si"}, - {"id": 4, "value": "Si"}, - {"id": 5, "value": "Si"}, - {"id": 6, "value": "Si"}, - {"id": 7, "value": "Si"}, - ], - "coordinates": [ - {"id": 0, "value": [0.5, 0.0, 0.0]}, - {"id": 1, "value": [0.25, 0.25, 0.75]}, - {"id": 2, "value": [0.5, 0.5, 0.5]}, - {"id": 3, "value": [0.25, 0.75, 0.25]}, - {"id": 4, "value": [0.0, 0.0, 0.5]}, - {"id": 5, "value": [0.75, 0.25, 0.25]}, - {"id": 6, "value": [0.0, 0.5, 0.0]}, - {"id": 7, "value": [0.75, 0.75, 0.75]}, - ], - "units": "crystal", - "cell": [[5.468763846, 0.0, 0.0], [-0.0, 5.468763846, 0.0], [0.0, 0.0, 5.468763846]], - "constraints": [], - "labels": [], - }, - "lattice": { - "a": 5.468763846, - "b": 5.468763846, - "c": 5.468763846, - "alpha": 90.0, - "beta": 90.0, - "gamma": 90.0, - "units": {"length": "angstrom", "angle": "degree"}, - "type": "TRI", - "vectors": { - "a": [5.468763846, 0.0, 0.0], - "b": [-0.0, 5.468763846, 0.0], - "c": [0.0, 0.0, 5.468763846], - "alat": 1, - "units": "angstrom", - }, - }, - "isNonPeriodic": False, - "_id": "", - "metadata": {"boundaryConditions": {"type": "pbc", "offset": 0}}, - "isUpdated": True, - }, - "miller_indices": (0, 0, 1), - "thickness": 1, - "vacuum": 5.0, - "xy_supercell_matrix": [[1, 0], [0, 1]], - "use_conventional_cell": True, - "use_orthogonal_z": True, - "make_primitive": True, - }, - }, + "build": {"configuration": SI_SLAB_CONFIGURATION, "termination": "Si_P6/mmm_1"}, }, "isUpdated": True, } diff --git a/tests/py/unit/test_tools_build_defect.py b/tests/py/unit/test_tools_build_defect.py index 03df8e9b..036cc127 100644 --- a/tests/py/unit/test_tools_build_defect.py +++ b/tests/py/unit/test_tools_build_defect.py @@ -107,7 +107,7 @@ def test_create_crystal_site_adatom(): assert defect.basis.elements.values[-1] == "Si" assertion_utils.assert_deep_almost_equal( - # TO pass on GHA + # Adjusted expected value to pass tests on GHA due to slab generation differences between GHA and local [0.083333333, 0.458333333, 0.561569594], defect.basis.coordinates.values[-1], ) diff --git a/tests/py/unit/test_tools_build_grain_boundary.py b/tests/py/unit/test_tools_build_grain_boundary.py index be3dbc1a..86ad9476 100644 --- a/tests/py/unit/test_tools_build_grain_boundary.py +++ b/tests/py/unit/test_tools_build_grain_boundary.py @@ -57,7 +57,7 @@ def test_slab_grain_boundary_builder(): [0.0, 3.867, 0.0], [0.0, 0.0, 8.734], ] - # To pass on GHA + # Adjusted expected value to pass tests on GHA due to slab generation differences between GHA and local expected_coordinate_15 = [0.777190818, 0.0, 0.110688115] assert len(gb.basis.elements.values) == 32 diff --git a/tests/py/unit/test_tools_build_slab.py b/tests/py/unit/test_tools_build_slab.py index 2c85bf40..5d1f2b82 100644 --- a/tests/py/unit/test_tools_build_slab.py +++ b/tests/py/unit/test_tools_build_slab.py @@ -19,5 +19,4 @@ def test_build_slab(): ) termination = get_terminations(slab_config)[0] slab = create_slab(slab_config, termination) - print(slab.to_json()) assertion_utils.assert_deep_almost_equal(SI_SLAB, slab.to_json()) diff --git a/tests/py/unit/test_tools_modify.py b/tests/py/unit/test_tools_modify.py index f22d7fd1..8446a594 100644 --- a/tests/py/unit/test_tools_modify.py +++ b/tests/py/unit/test_tools_modify.py @@ -5,15 +5,15 @@ from mat3ra.made.tools.convert.utils import InterfacePartsEnum from mat3ra.made.tools.modify import ( add_vacuum, - displace_interface_part, filter_by_circle_projection, filter_by_label, filter_by_layers, filter_by_rectangle_projection, filter_by_sphere, filter_by_triangle_projection, + interface_displace_part, remove_vacuum, - rotate_material, + rotate, translate_to_z_level, ) from mat3ra.utils import assertion as assertion_utils @@ -164,11 +164,11 @@ def test_remove_vacuum(): assertion_utils.assert_deep_almost_equal(material_down.to_json(), material_with_set_vacuum.to_json()) -def test_rotate_material(): +def test_rotate(): material = Material(SI_SLAB) # Rotation around Z and X axis will be equivalent for the original material for hist basis in terms of coordinates - rotated_material = rotate_material(material, [0, 0, 1], 180) - rotated_material = rotate_material(rotated_material, [1, 0, 0], 180) + rotated_material = rotate(material, [0, 0, 1], 180) + rotated_material = rotate(rotated_material, [1, 0, 0], 180) assertion_utils.assert_deep_almost_equal( material.basis.coordinates.values.sort(), rotated_material.basis.coordinates.values.sort() ) @@ -185,7 +185,7 @@ def test_displace_interface(): {"id": 4, "value": [0.766666667, 0.866666667, 0.911447347]}, ] expected_labels = GRAPHENE_NICKEL_INTERFACE["basis"]["labels"] - displaced_material = displace_interface_part( + displaced_material = interface_displace_part( material, [0.1, 0.2, 0.3], InterfacePartsEnum.FILM, use_cartesian_coordinates=False ) assertion_utils.assert_deep_almost_equal(expected_coordinates, displaced_material.basis.coordinates.to_dict()) @@ -206,6 +206,6 @@ def test_displace_interface_optimized(): optimal_displacement = get_optimal_film_displacement( material, grid_size_xy=(10, 10), grid_range_x=(-0.5, 0.5), grid_range_y=(-0.5, 0.5) ) - displaced_material = displace_interface_part(material, optimal_displacement, use_cartesian_coordinates=True) + displaced_material = interface_displace_part(material, optimal_displacement, use_cartesian_coordinates=True) assertion_utils.assert_deep_almost_equal(expected_coordinates, displaced_material.basis.coordinates.to_dict()) assertion_utils.assert_deep_almost_equal(expected_labels, displaced_material.basis.labels.to_dict())