From 27410b4341c483b055b223c0541316c2646b6d40 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 14 Aug 2024 20:13:20 -0700 Subject: [PATCH 01/89] feat: add undercoordiantein analysis --- src/py/mat3ra/made/tools/analyze.py | 35 ++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index 47dcc109..dc9cdadc 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -1,3 +1,4 @@ +import math from typing import Callable, List, Literal, Optional import numpy as np @@ -265,6 +266,7 @@ def get_atom_indices_with_condition_on_coordinates( def get_nearest_neighbors_atom_indices( material: Material, coordinate: Optional[List[float]] = None, + cutoff: float = 15.0, ) -> Optional[List[int]]: """ Returns the indices of direct neighboring atoms to a specified position in the material using Voronoi tessellation. @@ -272,6 +274,7 @@ def get_nearest_neighbors_atom_indices( Args: material (Material): The material object to find neighbors in. coordinate (List[float]): The position to find neighbors for. + cutoff (float): The cutoff radius for identifying neighbors. Returns: List[int]: A list of indices of neighboring atoms, or an empty list if no neighbors are found. @@ -281,7 +284,7 @@ def get_nearest_neighbors_atom_indices( structure = to_pymatgen(material) voronoi_nn = PymatgenVoronoiNN( tol=0.5, - cutoff=15.0, + cutoff=cutoff, allow_pathological=False, weight="solid_angle", extra_nn_info=True, @@ -324,3 +327,33 @@ def get_atomic_coordinates_extremum( coordinates = new_material.basis.coordinates.to_array_of_values_with_ids() values = [coord.value[{"x": 0, "y": 1, "z": 2}[axis]] for coord in coordinates] return getattr(np, extremum)(values) + + +def get_undercoordinated_atom_indices(material: Material) -> List[int]: + """ + Identify undercoordinated atoms in a material based on a coordination number threshold. + + Args: + material (Molecule): The material object as a pymatgen Molecule. + + Returns: + List[int]: Indices of undercoordinated atoms. + """ + neighbors_numbers = [] + undercoordinated_atom_indices = [] + average_coordination = 0 + for idx in material.basis.coordinates.ids: + coordinate = material.basis.coordinates.values[idx] + try: + neighbors_indices = get_nearest_neighbors_atom_indices(material, coordinate, cutoff=3) + except: + print("error") + continue + neighbors_numbers.append(len(neighbors_indices)) + average_coordination = (average_coordination * (idx + 1) + len(neighbors_indices)) / (idx + 2) + threshold = math.floor(average_coordination) + for idx, number in enumerate(neighbors_numbers): + if number < threshold: + undercoordinated_atom_indices.append(idx) + + return undercoordinated_atom_indices From 365bfd0ec52eb4fcec019adfec44dec7826e49a5 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 14 Aug 2024 20:13:37 -0700 Subject: [PATCH 02/89] feat: add passivation modfy: --- src/py/mat3ra/made/tools/modify.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index daefdd6b..62eba35a 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -1,11 +1,13 @@ from typing import Callable, List, Literal, Optional, Union +import numpy as np from mat3ra.made.material import Material from .analyze import ( get_atom_indices_with_condition_on_coordinates, get_atom_indices_within_radius_pbc, get_atomic_coordinates_extremum, + get_undercoordinated_atom_indices, ) from .convert import from_ase, to_ase from .third_party import ase_add_vacuum @@ -437,3 +439,15 @@ def rotate_material(material: Material, axis: List[int], angle: float) -> Materi atoms.wrap() return Material(from_ase(atoms)) + + +def passivate_surface(slab: Material, passivant: str, bond_length: float = 3.0): + passivated_slab = slab.clone() + undercoordinated_atom_indices = get_undercoordinated_atom_indices(passivated_slab) + for i in undercoordinated_atom_indices: + atom_coordinate = passivated_slab.basis.coordinates.values[i] + # TODO: change normal to be from the cloeses center of mass + atom_normal = np.array(passivated_slab.basis.coordinates.values[i]) - np.array([0.5, 0.5, 0.5]) + passivated_slab.add_atom(passivant, atom_coordinate + atom_normal * bond_length) + + return passivated_slab From 312786a8dda537206647388e4e08b9c18d44023f Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 14 Aug 2024 20:13:59 -0700 Subject: [PATCH 03/89] update: add atom to material --- src/py/mat3ra/made/material.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/py/mat3ra/made/material.py b/src/py/mat3ra/made/material.py index 1a5d0162..cdc13971 100644 --- a/src/py/mat3ra/made/material.py +++ b/src/py/mat3ra/made/material.py @@ -95,3 +95,8 @@ def set_coordinates(self, coordinates: List[List[float]]) -> None: new_basis = self.basis.copy() new_basis.coordinates.values = coordinates self.basis = new_basis + + def add_atom(self, element: str, coordinate: List[float]) -> None: + new_basis = self.basis.copy() + new_basis.add_atom(element, coordinate) + self.basis = new_basis From 16d83d65c6e09ce2a14b64cdb6672d058a8646e1 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 20 Aug 2024 19:24:39 -0700 Subject: [PATCH 04/89] update: add undercoordination function --- src/py/mat3ra/made/tools/analyze.py | 58 +++++++++++++++-------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index dc9cdadc..dd0889d6 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -1,5 +1,4 @@ -import math -from typing import Callable, List, Literal, Optional +from typing import Callable, List, Literal, Optional, Tuple import numpy as np @@ -283,17 +282,26 @@ def get_nearest_neighbors_atom_indices( coordinate = [0, 0, 0] structure = to_pymatgen(material) voronoi_nn = PymatgenVoronoiNN( - tol=0.5, + tol=0.1, cutoff=cutoff, allow_pathological=False, weight="solid_angle", extra_nn_info=True, - compute_adj_neighbors=True, + compute_adj_neighbors=False, ) - structure.append("X", coordinate, validate_proximity=False) - neighbors = voronoi_nn.get_nn_info(structure, len(structure.sites) - 1) + coordinates = material.basis.coordinates + coordinates.filter_by_values([coordinate]) + site_index = coordinates.ids[0] + remove_added = False + if site_index is None: + structure.append("X", coordinate, validate_proximity=False) + site_index = len(structure.sites) - 1 + remove_added = True + neighbors = voronoi_nn.get_nn_info(structure, site_index) neighboring_atoms_pymatgen_ids = [n["site_index"] for n in neighbors] - structure.remove_sites([-1]) + + if remove_added: + structure.remove_sites([-1]) all_coordinates = material.basis.coordinates all_coordinates.filter_by_indices(neighboring_atoms_pymatgen_ids) @@ -329,31 +337,27 @@ def get_atomic_coordinates_extremum( return getattr(np, extremum)(values) -def get_undercoordinated_atom_indices(material: Material) -> List[int]: - """ - Identify undercoordinated atoms in a material based on a coordination number threshold. - - Args: - material (Molecule): The material object as a pymatgen Molecule. - - Returns: - List[int]: Indices of undercoordinated atoms. - """ +def get_undercoordinated_atom_indices( + material: Material, indices_to_check: List[int] +) -> Tuple[List[int], List[List[int]]]: + neighbors_indices_array = [] neighbors_numbers = [] - undercoordinated_atom_indices = [] - average_coordination = 0 - for idx in material.basis.coordinates.ids: + undercoordinated_atom_indices: List[int] = [] + set_of_neighbors_numbers = set() + for idx in indices_to_check: coordinate = material.basis.coordinates.values[idx] + neighbors_indices: List[int] = [] try: - neighbors_indices = get_nearest_neighbors_atom_indices(material, coordinate, cutoff=3) - except: - print("error") + neighbors_indices = get_nearest_neighbors_atom_indices(material, coordinate, cutoff=5) + neighbors_indices_array.append(neighbors_indices) + except Exception as e: + print(f"Error: {e}") + neighbors_indices_array.append([]) continue neighbors_numbers.append(len(neighbors_indices)) - average_coordination = (average_coordination * (idx + 1) + len(neighbors_indices)) / (idx + 2) - threshold = math.floor(average_coordination) + set_of_neighbors_numbers.add(len(neighbors_indices)) + threshold = max(set_of_neighbors_numbers) for idx, number in enumerate(neighbors_numbers): if number < threshold: undercoordinated_atom_indices.append(idx) - - return undercoordinated_atom_indices + return undercoordinated_atom_indices, neighbors_indices_array From 35d6f865ebebe6b7e693a7a917bab37d917521fa Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 20 Aug 2024 19:25:09 -0700 Subject: [PATCH 05/89] update: fix hashable --- src/py/mat3ra/made/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/utils.py b/src/py/mat3ra/made/utils.py index 21d180a2..15aec478 100644 --- a/src/py/mat3ra/made/utils.py +++ b/src/py/mat3ra/made/utils.py @@ -95,8 +95,11 @@ def get_element_value_by_index(self, index: int) -> Any: return self.values[index] if index < len(self.values) else None def filter_by_values(self, values: Union[List[Any], Any]): - values_to_keep = set(values) if isinstance(values, list) else {values} - filtered_items = [(v, i) for v, i in zip(self.values, self.ids) if v in values_to_keep] + def make_hashable(value): + return tuple(value) if isinstance(value, list) else value + + values_to_keep = set(make_hashable(v) for v in values) if isinstance(values, list) else {make_hashable(values)} + filtered_items = [(v, i) for v, i in zip(self.values, self.ids) if make_hashable(v) in values_to_keep] if filtered_items: values_unpacked, ids_unpacked = zip(*filtered_items) self.values = list(values_unpacked) From 9a5e7c807dca3e1a6f484882865001c0821e6133 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 20 Aug 2024 19:33:04 -0700 Subject: [PATCH 06/89] feat: add passivation calculation --- src/py/mat3ra/made/tools/modify.py | 42 +++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index 62eba35a..84c08e49 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -2,6 +2,9 @@ import numpy as np from mat3ra.made.material import Material +from mat3ra.made.tools.build.supercell import create_supercell +from mat3ra.made.tools.utils import transform_coordinate_to_supercell +from mat3ra.made.utils import get_center_of_coordinates from .analyze import ( get_atom_indices_with_condition_on_coordinates, @@ -441,13 +444,34 @@ def rotate_material(material: Material, axis: List[int], angle: float) -> Materi return Material(from_ase(atoms)) -def passivate_surface(slab: Material, passivant: str, bond_length: float = 3.0): - passivated_slab = slab.clone() - undercoordinated_atom_indices = get_undercoordinated_atom_indices(passivated_slab) - for i in undercoordinated_atom_indices: - atom_coordinate = passivated_slab.basis.coordinates.values[i] - # TODO: change normal to be from the cloeses center of mass - atom_normal = np.array(passivated_slab.basis.coordinates.values[i]) - np.array([0.5, 0.5, 0.5]) - passivated_slab.add_atom(passivant, atom_coordinate + atom_normal * bond_length) +def passivate_surface(slab: Material, passivant: str, bond_length: float = 1.0): + supercell_sclaing_factor = [3, 3, 3] + centered_slab = translate_to_z_level(slab, "center") + supercell_slab = create_supercell(centered_slab, scaling_factor=supercell_sclaing_factor) - return passivated_slab + def central_cell_condition(coordinate): + return is_coordinate_in_box(coordinate, [1 / 3, 1 / 3, 1 / 3], [2 / 3, 2 / 3, 2 / 3]) + + indices_in_central_cell = [ + idx + for idx in supercell_slab.basis.coordinates.ids + if central_cell_condition(supercell_slab.basis.coordinates.values[idx]) + ] + + undercoordinated_atom_indices, neighbors_indices = get_undercoordinated_atom_indices( + supercell_slab, indices_in_central_cell + ) + + for index in undercoordinated_atom_indices: + atom_coordinate = supercell_slab.basis.coordinates.values[index] + neighbors_coordinates = [supercell_slab.basis.coordinates.values[j] for j in neighbors_indices[index]] + neighbors_average_coordinate = get_center_of_coordinates(neighbors_coordinates) + bond_normal = np.array(supercell_slab.basis.coordinates.values[index]) - np.array(neighbors_average_coordinate) + bond_vector_crystal = supercell_slab.basis.cell.convert_point_to_crystal(bond_normal * bond_length) + passivant_atom_coordinate_supercell = atom_coordinate + bond_vector_crystal + passivant_atom_coordinate = transform_coordinate_to_supercell( + passivant_atom_coordinate_supercell, scaling_factor=supercell_sclaing_factor, reverse=True + ) + + centered_slab.add_atom(passivant, passivant_atom_coordinate) + return centered_slab From 475c1cce1abbe6dd9b96ac60f84c9ee78b30d473 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 20 Aug 2024 19:34:18 -0700 Subject: [PATCH 07/89] chore: run lint fix --- src/py/mat3ra/made/tools/analyze.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index dd0889d6..0415e9a0 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -348,7 +348,7 @@ def get_undercoordinated_atom_indices( coordinate = material.basis.coordinates.values[idx] neighbors_indices: List[int] = [] try: - neighbors_indices = get_nearest_neighbors_atom_indices(material, coordinate, cutoff=5) + neighbors_indices = get_nearest_neighbors_atom_indices(material, coordinate, cutoff=5) or [] neighbors_indices_array.append(neighbors_indices) except Exception as e: print(f"Error: {e}") From 9757591e565ba0a318918a44fd1a9cd676275abc Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:09:16 -0700 Subject: [PATCH 08/89] update: temporarily two different nn calculations --- src/py/mat3ra/made/tools/analyze.py | 77 ++++++++++++++----- .../made/tools/build/defect/builders.py | 4 +- 2 files changed, 59 insertions(+), 22 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index 0415e9a0..fcd10353 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -262,7 +262,46 @@ def get_atom_indices_with_condition_on_coordinates( return selected_indices -def get_nearest_neighbors_atom_indices( +def get_nearest_neighbors_atom_indices_for_coordinate( + material: Material, + coordinate: Optional[List[float]] = None, + cutoff: float = 15.0, +) -> Optional[List[int]]: + """ + Returns the indices of direct neighboring atoms to a specified position in the material using Voronoi tessellation. + + Args: + material (Material): The material object to find neighbors in. + coordinate (List[float]): The position to find neighbors for. + cutoff (float): The cutoff radius for identifying neighbors. + + Returns: + List[int]: A list of indices of neighboring atoms, or an empty list if no neighbors are found. + """ + if coordinate is None: + coordinate = [0, 0, 0] + structure = to_pymatgen(material) + voronoi_nn = PymatgenVoronoiNN( + tol=0.5, + cutoff=cutoff, + allow_pathological=False, + weight="solid_angle", + extra_nn_info=False, + compute_adj_neighbors=False, + ) + + structure.append("X", coordinate, validate_proximity=False) + site_index = len(structure.sites) - 1 + neighbors = voronoi_nn.get_nn_info(structure, site_index) + neighboring_atoms_pymatgen_ids = [n["site_index"] for n in neighbors] + structure.remove_sites([-1]) + + all_coordinates = material.basis.coordinates + all_coordinates.filter_by_indices(neighboring_atoms_pymatgen_ids) + return all_coordinates.ids + + +def get_nearest_neighbors_atom_indices_for_atom( material: Material, coordinate: Optional[List[float]] = None, cutoff: float = 15.0, @@ -286,23 +325,15 @@ def get_nearest_neighbors_atom_indices( cutoff=cutoff, allow_pathological=False, weight="solid_angle", - extra_nn_info=True, + extra_nn_info=False, compute_adj_neighbors=False, ) coordinates = material.basis.coordinates coordinates.filter_by_values([coordinate]) site_index = coordinates.ids[0] - remove_added = False - if site_index is None: - structure.append("X", coordinate, validate_proximity=False) - site_index = len(structure.sites) - 1 - remove_added = True neighbors = voronoi_nn.get_nn_info(structure, site_index) neighboring_atoms_pymatgen_ids = [n["site_index"] for n in neighbors] - if remove_added: - structure.remove_sites([-1]) - all_coordinates = material.basis.coordinates all_coordinates.filter_by_indices(neighboring_atoms_pymatgen_ids) return all_coordinates.ids @@ -338,26 +369,32 @@ def get_atomic_coordinates_extremum( def get_undercoordinated_atom_indices( - material: Material, indices_to_check: List[int] + material: Material, indices_to_check: Optional[List[int]] = None ) -> Tuple[List[int], List[List[int]]]: + if indices_to_check is None: + indices_to_check = material.basis.coordinates.ids + + coordinates = np.array(material.basis.coordinates.values) neighbors_indices_array = [] neighbors_numbers = [] undercoordinated_atom_indices: List[int] = [] - set_of_neighbors_numbers = set() + for idx in indices_to_check: - coordinate = material.basis.coordinates.values[idx] - neighbors_indices: List[int] = [] + coordinate = coordinates[idx] try: - neighbors_indices = get_nearest_neighbors_atom_indices(material, coordinate, cutoff=5) or [] + neighbors_indices = ( + get_nearest_neighbors_atom_indices_for_atom(material, coordinate.tolist(), cutoff=5) or [] + ) neighbors_indices_array.append(neighbors_indices) except Exception as e: print(f"Error: {e}") neighbors_indices_array.append([]) continue neighbors_numbers.append(len(neighbors_indices)) - set_of_neighbors_numbers.add(len(neighbors_indices)) - threshold = max(set_of_neighbors_numbers) - for idx, number in enumerate(neighbors_numbers): - if number < threshold: - undercoordinated_atom_indices.append(idx) + + neighbors_numbers = np.array(neighbors_numbers) # type: ignore + threshold = np.max(neighbors_numbers) + + undercoordinated_atom_indices = np.where(neighbors_numbers < threshold)[0].tolist() + return undercoordinated_atom_indices, neighbors_indices_array diff --git a/src/py/mat3ra/made/tools/build/defect/builders.py b/src/py/mat3ra/made/tools/build/defect/builders.py index a9c25abd..39889ee9 100644 --- a/src/py/mat3ra/made/tools/build/defect/builders.py +++ b/src/py/mat3ra/made/tools/build/defect/builders.py @@ -24,7 +24,7 @@ from ...build import BaseBuilder from ...convert import to_pymatgen from ...analyze import ( - get_nearest_neighbors_atom_indices, + get_nearest_neighbors_atom_indices_for_coordinate, get_atomic_coordinates_extremum, get_closest_site_id_from_coordinate, get_closest_site_id_from_coordinate_and_element, @@ -245,7 +245,7 @@ def get_equidistant_position( coordinate=adatom_coordinate, scaling_factor=scaling_factor, translation_vector=translation_vector ) - neighboring_atoms_ids_in_supercell = get_nearest_neighbors_atom_indices( + neighboring_atoms_ids_in_supercell = get_nearest_neighbors_atom_indices_for_coordinate( supercell_material, adatom_coordinate_in_supercell ) if neighboring_atoms_ids_in_supercell is None: From 7895123bdeb2925f23c583222b5df4695834085a Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:09:43 -0700 Subject: [PATCH 09/89] update: passivate surface cirrectly --- src/py/mat3ra/made/tools/modify.py | 48 ++++++++++++++---------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index 84c08e49..be2cbbd9 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -444,34 +444,30 @@ def rotate_material(material: Material, axis: List[int], angle: float) -> Materi return Material(from_ase(atoms)) -def passivate_surface(slab: Material, passivant: str, bond_length: float = 1.0): - supercell_sclaing_factor = [3, 3, 3] +def passivate_surface_single_cell(slab: Material, passivant: str, bond_length: float = 1.0): + supercell_scaling_factor = [3, 3, 3] centered_slab = translate_to_z_level(slab, "center") - supercell_slab = create_supercell(centered_slab, scaling_factor=supercell_sclaing_factor) - - def central_cell_condition(coordinate): - return is_coordinate_in_box(coordinate, [1 / 3, 1 / 3, 1 / 3], [2 / 3, 2 / 3, 2 / 3]) - - indices_in_central_cell = [ - idx - for idx in supercell_slab.basis.coordinates.ids - if central_cell_condition(supercell_slab.basis.coordinates.values[idx]) - ] - + centered_slab_supercell = create_supercell(centered_slab, scaling_factor=supercell_scaling_factor) undercoordinated_atom_indices, neighbors_indices = get_undercoordinated_atom_indices( - supercell_slab, indices_in_central_cell + centered_slab_supercell, centered_slab_supercell.basis.coordinates.ids ) - + new_basis = centered_slab_supercell.basis.copy() for index in undercoordinated_atom_indices: - atom_coordinate = supercell_slab.basis.coordinates.values[index] - neighbors_coordinates = [supercell_slab.basis.coordinates.values[j] for j in neighbors_indices[index]] - neighbors_average_coordinate = get_center_of_coordinates(neighbors_coordinates) - bond_normal = np.array(supercell_slab.basis.coordinates.values[index]) - np.array(neighbors_average_coordinate) - bond_vector_crystal = supercell_slab.basis.cell.convert_point_to_crystal(bond_normal * bond_length) - passivant_atom_coordinate_supercell = atom_coordinate + bond_vector_crystal - passivant_atom_coordinate = transform_coordinate_to_supercell( - passivant_atom_coordinate_supercell, scaling_factor=supercell_sclaing_factor, reverse=True + atom_coordinate_crystal = centered_slab_supercell.basis.coordinates.values[index] + neighbors_coordinates_crystal = [ + centered_slab_supercell.basis.coordinates.values[j] for j in neighbors_indices[index] + ] + neighbors_average_coordinate_crystal = get_center_of_coordinates(neighbors_coordinates_crystal) + + bond_normal_crystal = np.array(centered_slab_supercell.basis.coordinates.values[index]) - np.array( + neighbors_average_coordinate_crystal ) - - centered_slab.add_atom(passivant, passivant_atom_coordinate) - return centered_slab + bond_normal_cartesian = centered_slab_supercell.basis.cell.convert_point_to_cartesian(bond_normal_crystal) + bond_vector_cartesian = bond_normal_cartesian * bond_length + bond_vector_crystal = centered_slab_supercell.basis.cell.convert_point_to_crystal(bond_vector_cartesian) + passivant_atom_coordinate = atom_coordinate_crystal + bond_vector_crystal + new_basis.add_atom(passivant, passivant_atom_coordinate) + + centered_slab_supercell.basis = new_basis + centered_slab_supercell = filter_by_box(centered_slab_supercell, [1 / 3, 1 / 3, 1 / 3], [2 / 3, 2 / 3, 2 / 3]) + return centered_slab_supercell From e646efb0072c915f6ca033ee16ab835f12d12a47 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 20 Aug 2024 21:10:13 -0700 Subject: [PATCH 10/89] chore: run lint fix --- src/py/mat3ra/made/tools/modify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index be2cbbd9..3210426f 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -3,7 +3,6 @@ import numpy as np from mat3ra.made.material import Material from mat3ra.made.tools.build.supercell import create_supercell -from mat3ra.made.tools.utils import transform_coordinate_to_supercell from mat3ra.made.utils import get_center_of_coordinates from .analyze import ( @@ -444,7 +443,7 @@ def rotate_material(material: Material, axis: List[int], angle: float) -> Materi return Material(from_ase(atoms)) -def passivate_surface_single_cell(slab: Material, passivant: str, bond_length: float = 1.0): +def passivate_surface(slab: Material, passivant: str, bond_length: float = 1.0): supercell_scaling_factor = [3, 3, 3] centered_slab = translate_to_z_level(slab, "center") centered_slab_supercell = create_supercell(centered_slab, scaling_factor=supercell_scaling_factor) From bd34d44cb63d23164146b2212c6e459de7440f5b Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 21 Aug 2024 17:26:12 -0700 Subject: [PATCH 11/89] chore: cleanup --- src/py/mat3ra/made/tools/modify.py | 37 +++++++++++++++--------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index 58f7ef73..e1333baa 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -470,28 +470,29 @@ def rotate_material(material: Material, axis: List[int], angle: float) -> Materi def passivate_surface(slab: Material, passivant: str, bond_length: float = 1.0): supercell_scaling_factor = [3, 3, 3] - centered_slab = translate_to_z_level(slab, "center") - centered_slab_supercell = create_supercell(centered_slab, scaling_factor=supercell_scaling_factor) + min_coordinate = [1 / 3, 1 / 3, 1 / 3] + max_coordinate = [2 / 3, 2 / 3, 2 / 3] + slab = translate_to_z_level(slab, "center") + slab_supercell = create_supercell(slab, scaling_factor=supercell_scaling_factor) undercoordinated_atom_indices, neighbors_indices = get_undercoordinated_atom_indices( - centered_slab_supercell, centered_slab_supercell.basis.coordinates.ids + slab_supercell, slab_supercell.basis.coordinates.ids ) - new_basis = centered_slab_supercell.basis.copy() + new_basis = slab_supercell.basis.copy() for index in undercoordinated_atom_indices: - atom_coordinate_crystal = centered_slab_supercell.basis.coordinates.values[index] - neighbors_coordinates_crystal = [ - centered_slab_supercell.basis.coordinates.values[j] for j in neighbors_indices[index] - ] + atom_coordinate_crystal = slab_supercell.basis.coordinates.values[index] + neighbors_coordinates_crystal = [slab_supercell.basis.coordinates.values[j] for j in neighbors_indices[index]] neighbors_average_coordinate_crystal = get_center_of_coordinates(neighbors_coordinates_crystal) - bond_normal_crystal = np.array(centered_slab_supercell.basis.coordinates.values[index]) - np.array( + local_normal_crystal = np.array(slab_supercell.basis.coordinates.values[index]) - np.array( neighbors_average_coordinate_crystal ) - bond_normal_cartesian = centered_slab_supercell.basis.cell.convert_point_to_cartesian(bond_normal_crystal) - bond_vector_cartesian = bond_normal_cartesian * bond_length - bond_vector_crystal = centered_slab_supercell.basis.cell.convert_point_to_crystal(bond_vector_cartesian) - passivant_atom_coordinate = atom_coordinate_crystal + bond_vector_crystal - new_basis.add_atom(passivant, passivant_atom_coordinate) - - centered_slab_supercell.basis = new_basis - centered_slab_supercell = filter_by_box(centered_slab_supercell, [1 / 3, 1 / 3, 1 / 3], [2 / 3, 2 / 3, 2 / 3]) - return centered_slab_supercell + local_normal_cartesian = slab_supercell.basis.cell.convert_point_to_cartesian(local_normal_crystal) + local_normal_cartesian /= np.linalg.norm(local_normal_cartesian) + bond_vector_cartesian = local_normal_cartesian * bond_length + bond_vector_crystal = slab_supercell.basis.cell.convert_point_to_crystal(bond_vector_cartesian) + passivant_atom_coordinate_crystal = atom_coordinate_crystal + bond_vector_crystal + new_basis.add_atom(passivant, passivant_atom_coordinate_crystal) + + slab_supercell.basis = new_basis + slab_supercell = filter_by_box(slab_supercell, min_coordinate, max_coordinate) + return slab_supercell From ac7a67b2161f478a07134b7a350b1c96defa353b Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 21 Aug 2024 17:28:17 -0700 Subject: [PATCH 12/89] update: generalize for supercells --- src/py/mat3ra/made/tools/analyze.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index fcd10353..d3d5d1d2 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -377,7 +377,6 @@ def get_undercoordinated_atom_indices( coordinates = np.array(material.basis.coordinates.values) neighbors_indices_array = [] neighbors_numbers = [] - undercoordinated_atom_indices: List[int] = [] for idx in indices_to_check: coordinate = coordinates[idx] @@ -395,6 +394,8 @@ def get_undercoordinated_atom_indices( neighbors_numbers = np.array(neighbors_numbers) # type: ignore threshold = np.max(neighbors_numbers) - undercoordinated_atom_indices = np.where(neighbors_numbers < threshold)[0].tolist() + undercoordinated_atom_indices = [ + idx for idx, num_neighbors in zip(indices_to_check, neighbors_numbers) if num_neighbors < threshold + ] return undercoordinated_atom_indices, neighbors_indices_array From 9dc1880d5d0e256ab2374735990d66bd6c4283b7 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 21 Aug 2024 19:14:44 -0700 Subject: [PATCH 13/89] update: make work for 3d materials --- src/py/mat3ra/made/tools/analyze.py | 22 +++++++----- src/py/mat3ra/made/tools/modify.py | 55 ++++++++++++++++++----------- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index d3d5d1d2..c4b6a388 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -1,6 +1,7 @@ from typing import Callable, List, Literal, Optional, Tuple import numpy as np +from mat3ra.made.utils import get_center_of_coordinates from ..material import Material from .convert import decorator_convert_material_args_kwargs_to_atoms, to_pymatgen @@ -370,32 +371,37 @@ def get_atomic_coordinates_extremum( def get_undercoordinated_atom_indices( material: Material, indices_to_check: Optional[List[int]] = None -) -> Tuple[List[int], List[List[int]]]: +) -> Tuple[List[int], dict]: if indices_to_check is None: indices_to_check = material.basis.coordinates.ids coordinates = np.array(material.basis.coordinates.values) neighbors_indices_array = [] + neighbors_center_coordinates_array = [] neighbors_numbers = [] + atom_neighbors_info = {} + print("Indices to check", indices_to_check, len(indices_to_check)) for idx in indices_to_check: coordinate = coordinates[idx] try: neighbors_indices = ( - get_nearest_neighbors_atom_indices_for_atom(material, coordinate.tolist(), cutoff=5) or [] + get_nearest_neighbors_atom_indices_for_atom(material, coordinate.tolist(), cutoff=9) or [] ) - neighbors_indices_array.append(neighbors_indices) + neighbors_coordinates = np.array([coordinates[i] for i in neighbors_indices]) + neighbors_center = get_center_of_coordinates(neighbors_coordinates) + neighbors_indices_array.append({idx: neighbors_indices}) + neighbors_center_coordinates_array.append({idx: neighbors_center}) + atom_neighbors_info[idx] = (len(neighbors_indices), neighbors_indices, neighbors_center) except Exception as e: print(f"Error: {e}") - neighbors_indices_array.append([]) + neighbors_indices_array.append({idx: []}) continue neighbors_numbers.append(len(neighbors_indices)) - neighbors_numbers = np.array(neighbors_numbers) # type: ignore + neighbors_numbers = np.array(neighbors_numbers) threshold = np.max(neighbors_numbers) - undercoordinated_atom_indices = [ idx for idx, num_neighbors in zip(indices_to_check, neighbors_numbers) if num_neighbors < threshold ] - - return undercoordinated_atom_indices, neighbors_indices_array + return undercoordinated_atom_indices, atom_neighbors_info diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index e1333baa..ba7410c4 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -3,7 +3,7 @@ import numpy as np from mat3ra.made.material import Material from mat3ra.made.tools.build.supercell import create_supercell -from mat3ra.made.utils import get_center_of_coordinates +from mat3ra.made.tools.utils import transform_coordinate_to_supercell from .analyze import ( get_atom_indices_with_condition_on_coordinates, @@ -468,31 +468,46 @@ def rotate_material(material: Material, axis: List[int], angle: float) -> Materi return Material(from_ase(atoms)) -def passivate_surface(slab: Material, passivant: str, bond_length: float = 1.0): +def get_passivant_coordinate_crystal( + material: Material, atom_coordinate: np.ndarray, bond_direction: np.ndarray, bond_length: float = 2.0 +): + bond_normal_cartesian = np.array(material.basis.cell.convert_point_to_cartesian(bond_direction.tolist())) + bond_normal_cartesian /= np.linalg.norm(bond_normal_cartesian) + bond_vector_cartesian = bond_normal_cartesian * bond_length + bond_vector_crystal = np.array(material.basis.cell.convert_point_to_crystal(bond_vector_cartesian.tolist())) + passivant_coordinate_crystal = atom_coordinate + bond_vector_crystal + return passivant_coordinate_crystal + + +def passivate_surface(slab: Material, passivant: str, bond_length: float = 2.0): supercell_scaling_factor = [3, 3, 3] min_coordinate = [1 / 3, 1 / 3, 1 / 3] max_coordinate = [2 / 3, 2 / 3, 2 / 3] slab = translate_to_z_level(slab, "center") + new_basis = slab.basis.copy() slab_supercell = create_supercell(slab, scaling_factor=supercell_scaling_factor) - undercoordinated_atom_indices, neighbors_indices = get_undercoordinated_atom_indices( - slab_supercell, slab_supercell.basis.coordinates.ids + + central_cell_ids = [ + id + for id, coordinate in zip(slab_supercell.basis.coordinates.ids, slab_supercell.basis.coordinates.values) + if is_coordinate_in_box(coordinate, min_coordinate, max_coordinate) + ] + undercoordinated_atom_indices, atom_neighbors_info = get_undercoordinated_atom_indices( + slab_supercell, central_cell_ids ) - new_basis = slab_supercell.basis.copy() for index in undercoordinated_atom_indices: - atom_coordinate_crystal = slab_supercell.basis.coordinates.values[index] - neighbors_coordinates_crystal = [slab_supercell.basis.coordinates.values[j] for j in neighbors_indices[index]] - neighbors_average_coordinate_crystal = get_center_of_coordinates(neighbors_coordinates_crystal) - - local_normal_crystal = np.array(slab_supercell.basis.coordinates.values[index]) - np.array( + atom_coordinate_crystal = np.array(slab_supercell.basis.coordinates.values[index]) + neighbors_average_coordinate_crystal = atom_neighbors_info[index][2] + bond_normal_crystal = np.array(slab_supercell.basis.coordinates.values[index]) - np.array( neighbors_average_coordinate_crystal ) - local_normal_cartesian = slab_supercell.basis.cell.convert_point_to_cartesian(local_normal_crystal) - local_normal_cartesian /= np.linalg.norm(local_normal_cartesian) - bond_vector_cartesian = local_normal_cartesian * bond_length - bond_vector_crystal = slab_supercell.basis.cell.convert_point_to_crystal(bond_vector_cartesian) - passivant_atom_coordinate_crystal = atom_coordinate_crystal + bond_vector_crystal - new_basis.add_atom(passivant, passivant_atom_coordinate_crystal) - - slab_supercell.basis = new_basis - slab_supercell = filter_by_box(slab_supercell, min_coordinate, max_coordinate) - return slab_supercell + passivant_coordinate_crystal = get_passivant_coordinate_crystal( + slab_supercell, atom_coordinate_crystal, bond_normal_crystal, bond_length + ).tolist() + passivant_coordinate_crystal_original_cell = transform_coordinate_to_supercell( + passivant_coordinate_crystal, scaling_factor=supercell_scaling_factor, reverse=True + ) + new_basis.add_atom(passivant, passivant_coordinate_crystal_original_cell) + + slab.basis = new_basis + return slab From 6854d54e5fe54ed3921107cd958621c75e0a0f46 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 21 Aug 2024 19:34:40 -0700 Subject: [PATCH 14/89] update: cleanup --- src/py/mat3ra/made/tools/analyze.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index c4b6a388..e7857d81 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -375,33 +375,25 @@ def get_undercoordinated_atom_indices( if indices_to_check is None: indices_to_check = material.basis.coordinates.ids - coordinates = np.array(material.basis.coordinates.values) - neighbors_indices_array = [] - neighbors_center_coordinates_array = [] - neighbors_numbers = [] - + coordinates_array = material.basis.coordinates.values + number_of_neighbors = [] atom_neighbors_info = {} - print("Indices to check", indices_to_check, len(indices_to_check)) for idx in indices_to_check: - coordinate = coordinates[idx] + coordinate = coordinates_array[idx] try: neighbors_indices = ( get_nearest_neighbors_atom_indices_for_atom(material, coordinate.tolist(), cutoff=9) or [] ) - neighbors_coordinates = np.array([coordinates[i] for i in neighbors_indices]) + neighbors_coordinates = [coordinates_array[i] for i in neighbors_indices] neighbors_center = get_center_of_coordinates(neighbors_coordinates) - neighbors_indices_array.append({idx: neighbors_indices}) - neighbors_center_coordinates_array.append({idx: neighbors_center}) atom_neighbors_info[idx] = (len(neighbors_indices), neighbors_indices, neighbors_center) except Exception as e: print(f"Error: {e}") - neighbors_indices_array.append({idx: []}) continue - neighbors_numbers.append(len(neighbors_indices)) + number_of_neighbors.append(len(neighbors_indices)) - neighbors_numbers = np.array(neighbors_numbers) - threshold = np.max(neighbors_numbers) + threshold = np.max(number_of_neighbors) undercoordinated_atom_indices = [ - idx for idx, num_neighbors in zip(indices_to_check, neighbors_numbers) if num_neighbors < threshold + idx for idx, num_neighbors in zip(indices_to_check, number_of_neighbors) if num_neighbors < threshold ] return undercoordinated_atom_indices, atom_neighbors_info From 9fa3833eb0ee13637038ab12dc02232d52eac72d Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 21 Aug 2024 20:25:47 -0700 Subject: [PATCH 15/89] update: correctly transform between central cell --- src/py/mat3ra/made/tools/modify.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index ba7410c4..d5735c48 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -480,9 +480,12 @@ def get_passivant_coordinate_crystal( def passivate_surface(slab: Material, passivant: str, bond_length: float = 2.0): + nudge_value = 0.01 supercell_scaling_factor = [3, 3, 3] min_coordinate = [1 / 3, 1 / 3, 1 / 3] max_coordinate = [2 / 3, 2 / 3, 2 / 3] + adjusted_min_coordinate = (np.array(min_coordinate) - nudge_value).tolist() + adjusted_max_coordinate = (np.array(max_coordinate) + nudge_value).tolist() slab = translate_to_z_level(slab, "center") new_basis = slab.basis.copy() slab_supercell = create_supercell(slab, scaling_factor=supercell_scaling_factor) @@ -490,7 +493,7 @@ def passivate_surface(slab: Material, passivant: str, bond_length: float = 2.0): central_cell_ids = [ id for id, coordinate in zip(slab_supercell.basis.coordinates.ids, slab_supercell.basis.coordinates.values) - if is_coordinate_in_box(coordinate, min_coordinate, max_coordinate) + if is_coordinate_in_box(coordinate, adjusted_min_coordinate, adjusted_max_coordinate) ] undercoordinated_atom_indices, atom_neighbors_info = get_undercoordinated_atom_indices( slab_supercell, central_cell_ids @@ -505,7 +508,10 @@ def passivate_surface(slab: Material, passivant: str, bond_length: float = 2.0): slab_supercell, atom_coordinate_crystal, bond_normal_crystal, bond_length ).tolist() passivant_coordinate_crystal_original_cell = transform_coordinate_to_supercell( - passivant_coordinate_crystal, scaling_factor=supercell_scaling_factor, reverse=True + passivant_coordinate_crystal, + scaling_factor=supercell_scaling_factor, + translation_vector=min_coordinate, + reverse=True, ) new_basis.add_atom(passivant, passivant_coordinate_crystal_original_cell) From 1912a0a8aab16e6f9e9bffaa61fd45bbaec80f68 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 21 Aug 2024 21:40:01 -0700 Subject: [PATCH 16/89] feat: add simple passivation --- src/py/mat3ra/made/tools/modify.py | 69 ++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index d5735c48..5c139b62 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -9,7 +9,7 @@ get_atom_indices_with_condition_on_coordinates, get_atom_indices_within_radius_pbc, get_atomic_coordinates_extremum, - get_undercoordinated_atom_indices, + get_undercoordinated_atoms, ) from .convert import from_ase, to_ase from .third_party import ase_add_vacuum @@ -479,7 +479,9 @@ def get_passivant_coordinate_crystal( return passivant_coordinate_crystal -def passivate_surface(slab: Material, passivant: str, bond_length: float = 2.0): +def passivate_material( + slab: Material, passivant: str, bond_length: float = 2.0, coordination_threshold: Optional[int] = None +): nudge_value = 0.01 supercell_scaling_factor = [3, 3, 3] min_coordinate = [1 / 3, 1 / 3, 1 / 3] @@ -495,8 +497,8 @@ def passivate_surface(slab: Material, passivant: str, bond_length: float = 2.0): for id, coordinate in zip(slab_supercell.basis.coordinates.ids, slab_supercell.basis.coordinates.values) if is_coordinate_in_box(coordinate, adjusted_min_coordinate, adjusted_max_coordinate) ] - undercoordinated_atom_indices, atom_neighbors_info = get_undercoordinated_atom_indices( - slab_supercell, central_cell_ids + undercoordinated_atom_indices, atom_neighbors_info = get_undercoordinated_atoms( + slab_supercell, central_cell_ids, coordination_threshold ) for index in undercoordinated_atom_indices: atom_coordinate_crystal = np.array(slab_supercell.basis.coordinates.values[index]) @@ -517,3 +519,62 @@ def passivate_surface(slab: Material, passivant: str, bond_length: float = 2.0): slab.basis = new_basis return slab + + +# TODO: Get this from peroidic table +BOND_LENGTHS_MAP = { + ("C", "H"): 1.09, + ("Ni", "H"): 1.09, + ("Si", "H"): 1.48, +} + + +def passivate_surface(material: Material, passivant: str = "H", default_bond_length: float = 1.0) -> Material: + """ + Passivates the top and bottom surfaces of a material by adding atoms along the Z-axis, + with bond lengths determined by the element and passivant. + + Args: + material (Material): The material to passivate. + passivant (str): The chemical symbol of the passivating atom (e.g., 'H'). + default_bond_length (float): The default bond length to use if the pair is not found in BOND_LENGTHS_MAP. + + Returns: + Material: The passivated material. + """ + + material = translate_to_z_level(material, "center") + coordinates = material.basis.coordinates.values + top_z = get_atomic_coordinates_extremum(material, "max", "z") + bottom_z = get_atomic_coordinates_extremum(material, "min", "z") + + tolerance = 0.01 + + top_surface_atoms = filter_by_box(material, [0, 0, top_z - tolerance], [1, 1, top_z + tolerance]) + top_surface_indices = top_surface_atoms.basis.coordinates.ids + + bottom_surface_atoms = filter_by_box(material, [0, 0, bottom_z - tolerance], [1, 1, bottom_z + tolerance]) + bottom_surface_indices = bottom_surface_atoms.basis.coordinates.ids + + new_basis = material.basis.copy() + + for idx in top_surface_indices: + atom_coordinate = coordinates[idx] + element = material.basis.elements.values[idx] + bond_length = BOND_LENGTHS_MAP.get((element, passivant), default_bond_length) + + bond_length_crystal = material.basis.cell.convert_point_to_crystal([0, 0, bond_length]) + passivant_coordinate = atom_coordinate + bond_length_crystal + new_basis.add_atom(passivant, passivant_coordinate) + + for idx in bottom_surface_indices: + atom_coordinate = coordinates[idx] + element = material.basis.elements.values[idx] + bond_length = BOND_LENGTHS_MAP.get((element, passivant), default_bond_length) + + bond_length_crystal = material.basis.cell.convert_point_to_crystal([0, 0, bond_length]) + passivant_coordinate = atom_coordinate - bond_length_crystal + new_basis.add_atom(passivant, passivant_coordinate) + + material.basis = new_basis + return material From 05bbe350dd94dce9f5f5537b442979ee96dbd596 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 21 Aug 2024 21:40:27 -0700 Subject: [PATCH 17/89] update: optimize and add threshold --- src/py/mat3ra/made/tools/analyze.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index e7857d81..a5968357 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -369,8 +369,8 @@ def get_atomic_coordinates_extremum( return getattr(np, extremum)(values) -def get_undercoordinated_atom_indices( - material: Material, indices_to_check: Optional[List[int]] = None +def get_undercoordinated_atoms( + material: Material, indices_to_check: Optional[List[int]] = None, coordination_threshold: Optional[int] = None ) -> Tuple[List[int], dict]: if indices_to_check is None: indices_to_check = material.basis.coordinates.ids @@ -380,20 +380,17 @@ def get_undercoordinated_atom_indices( atom_neighbors_info = {} for idx in indices_to_check: coordinate = coordinates_array[idx] - try: - neighbors_indices = ( - get_nearest_neighbors_atom_indices_for_atom(material, coordinate.tolist(), cutoff=9) or [] - ) - neighbors_coordinates = [coordinates_array[i] for i in neighbors_indices] - neighbors_center = get_center_of_coordinates(neighbors_coordinates) - atom_neighbors_info[idx] = (len(neighbors_indices), neighbors_indices, neighbors_center) - except Exception as e: - print(f"Error: {e}") - continue + neighbors_indices = get_nearest_neighbors_atom_indices_for_atom(material, coordinate, cutoff=6) or [] + neighbors_coordinates = [coordinates_array[i] for i in neighbors_indices] + neighbors_center = get_center_of_coordinates(neighbors_coordinates) + atom_neighbors_info[idx] = (len(neighbors_indices), neighbors_indices, neighbors_center) number_of_neighbors.append(len(neighbors_indices)) - threshold = np.max(number_of_neighbors) + if coordination_threshold is None: + coordination_threshold = np.max(number_of_neighbors) undercoordinated_atom_indices = [ - idx for idx, num_neighbors in zip(indices_to_check, number_of_neighbors) if num_neighbors < threshold + idx + for idx, num_neighbors in zip(indices_to_check, number_of_neighbors) + if num_neighbors < coordination_threshold ] return undercoordinated_atom_indices, atom_neighbors_info From 66c1b7ccd107bcdcafb521ac88781f88e431c5c7 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 22 Aug 2024 13:44:48 -0700 Subject: [PATCH 18/89] update: give cotnrol for both surfaces --- src/py/mat3ra/made/tools/modify.py | 72 +++++++++++++++++++----------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index 5c139b62..99dfb068 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import Callable, List, Literal, Optional, Union import numpy as np @@ -529,7 +530,18 @@ def passivate_material( } -def passivate_surface(material: Material, passivant: str = "H", default_bond_length: float = 1.0) -> Material: +class SURFACE_TYPES(str, Enum): + TOP = "top" + BOTTOM = "bottom" + BOTH = "both" + + +def passivate_surface( + material: Material, + passivant: str = "H", + default_bond_length: float = 1.0, + surface: SURFACE_TYPES = SURFACE_TYPES.BOTH, +) -> Material: """ Passivates the top and bottom surfaces of a material by adding atoms along the Z-axis, with bond lengths determined by the element and passivant. @@ -538,43 +550,49 @@ def passivate_surface(material: Material, passivant: str = "H", default_bond_len material (Material): The material to passivate. passivant (str): The chemical symbol of the passivating atom (e.g., 'H'). default_bond_length (float): The default bond length to use if the pair is not found in BOND_LENGTHS_MAP. + surface (SURFACE_TYPES): The surface to passivate ("top", "bottom", or "both"). Returns: Material: The passivated material. """ - material = translate_to_z_level(material, "center") - coordinates = material.basis.coordinates.values - top_z = get_atomic_coordinates_extremum(material, "max", "z") - bottom_z = get_atomic_coordinates_extremum(material, "min", "z") - tolerance = 0.01 + new_basis = material.basis.copy() - top_surface_atoms = filter_by_box(material, [0, 0, top_z - tolerance], [1, 1, top_z + tolerance]) - top_surface_indices = top_surface_atoms.basis.coordinates.ids + if surface in [SURFACE_TYPES.TOP, SURFACE_TYPES.BOTH]: + add_passivant_atoms_to_basis(material, SURFACE_TYPES.TOP, tolerance, passivant, default_bond_length, new_basis) + if surface in [SURFACE_TYPES.BOTTOM, SURFACE_TYPES.BOTH]: + add_passivant_atoms_to_basis( + material, SURFACE_TYPES.BOTTOM, tolerance, passivant, default_bond_length, new_basis + ) - bottom_surface_atoms = filter_by_box(material, [0, 0, bottom_z - tolerance], [1, 1, bottom_z + tolerance]) - bottom_surface_indices = bottom_surface_atoms.basis.coordinates.ids + material.basis = new_basis + return material - new_basis = material.basis.copy() - for idx in top_surface_indices: - atom_coordinate = coordinates[idx] - element = material.basis.elements.values[idx] - bond_length = BOND_LENGTHS_MAP.get((element, passivant), default_bond_length) - - bond_length_crystal = material.basis.cell.convert_point_to_crystal([0, 0, bond_length]) - passivant_coordinate = atom_coordinate + bond_length_crystal - new_basis.add_atom(passivant, passivant_coordinate) +def add_passivant_atoms_to_basis( + material: Material, z_level: SURFACE_TYPES, tolerance: float, passivant: str, default_bond_length: float, basis +) -> None: + """ + Add passivant atoms to the specified z-level (top or bottom) of the material. - for idx in bottom_surface_indices: - atom_coordinate = coordinates[idx] + Args: + material (Material): The material object to add passivant atoms to. + z_level (str): The z-level to add passivant atoms to ("top" or "bottom"). + tolerance (float): The tolerance for selecting surface atoms. + passivant (str): The chemical symbol of the passivating atom (e.g., 'H'). + default_bond_length (float): The default bond length to use if the pair is not found in BOND_LENGTHS_MAP. + basis: The basis of the new material to which passivant atoms will be added. + """ + z_extremum = get_atomic_coordinates_extremum(material, "max" if z_level == "top" else "min", "z") + surface_atoms = filter_by_box(material, [0, 0, z_extremum - tolerance], [1, 1, z_extremum + tolerance]) + surface_indices = surface_atoms.basis.coordinates.ids + for idx in surface_indices: + atom_coordinate = material.basis.coordinates.values[idx] element = material.basis.elements.values[idx] bond_length = BOND_LENGTHS_MAP.get((element, passivant), default_bond_length) - bond_length_crystal = material.basis.cell.convert_point_to_crystal([0, 0, bond_length]) - passivant_coordinate = atom_coordinate - bond_length_crystal - new_basis.add_atom(passivant, passivant_coordinate) - - material.basis = new_basis - return material + passivant_coordinate = ( + atom_coordinate + bond_length_crystal if z_level == "top" else atom_coordinate - bond_length_crystal + ) + basis.add_atom(passivant, passivant_coordinate) From acf43deef7da674c03beb76889f0e0ed49ed3446 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:07:45 -0700 Subject: [PATCH 19/89] update: add tests for passivation --- tests/py/unit/fixtures.py | 48 ++++++++++++++++++++++++++++++ tests/py/unit/test_tools_modify.py | 11 ++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/tests/py/unit/fixtures.py b/tests/py/unit/fixtures.py index d857147f..112bd810 100644 --- a/tests/py/unit/fixtures.py +++ b/tests/py/unit/fixtures.py @@ -226,6 +226,54 @@ "isUpdated": True, } + +SI_SLAB_PASSIVATED = { + "name": "Si8(001), termination Si_P4/mmm_1, Slab", + "basis": { + "elements": [ + {"id": 0, "value": "Si"}, + {"id": 1, "value": "Si"}, + {"id": 2, "value": "Si"}, + {"id": 3, "value": "Si"}, + {"id": 4, "value": "H"}, + {"id": 5, "value": "H"}, + ], + "coordinates": [ + {"id": 0, "value": [0.5, 0.5, 0.312499993]}, + {"id": 1, "value": [0.5, 0.0, 0.437499993]}, + {"id": 2, "value": [0.0, 0.0, 0.562499993]}, + {"id": 3, "value": [0.0, 0.5, 0.687499993]}, + {"id": 4, "value": [0.0, 0.5, 0.822813932]}, + {"id": 5, "value": [0.5, 0.5, 0.177186054]}, + ], + "units": "crystal", + "cell": [[3.867, 0.0, 0.0], [-0.0, 3.867, 0.0], [0.0, 0.0, 10.937528]], + "labels": [], + }, + "lattice": { + "a": 3.867, + "b": 3.867, + "c": 10.937527692, + "alpha": 90.0, + "beta": 90.0, + "gamma": 90.0, + "units": {"length": "angstrom", "angle": "degree"}, + "type": "TRI", + "vectors": { + "a": [3.867, 0.0, 0.0], + "b": [-0.0, 3.867, 0.0], + "c": [0.0, 0.0, 10.937527692], + "alat": 1, + "units": "angstrom", + }, + }, + "isNonPeriodic": False, + "_id": "", + "metadata": SI_SLAB["metadata"], + "isUpdated": True, +} + + SI_SLAB_VACUUM = copy.deepcopy(SI_SLAB) SI_SLAB_VACUUM["basis"]["coordinates"] = [ {"id": 0, "value": [0.5, 0.5, 0.386029718]}, diff --git a/tests/py/unit/test_tools_modify.py b/tests/py/unit/test_tools_modify.py index 9c848c2e..a4dc76bf 100644 --- a/tests/py/unit/test_tools_modify.py +++ b/tests/py/unit/test_tools_modify.py @@ -1,6 +1,7 @@ from ase.build import bulk from mat3ra.made.material import Material from mat3ra.made.tools.convert import from_ase + from mat3ra.made.tools.modify import ( add_vacuum, filter_by_circle_projection, @@ -12,10 +13,11 @@ remove_vacuum, rotate_material, translate_to_z_level, + passivate_surface, ) from mat3ra.utils import assertion as assertion_utils -from .fixtures import SI_CONVENTIONAL_CELL, SI_SLAB, SI_SLAB_VACUUM +from .fixtures import SI_CONVENTIONAL_CELL, SI_SLAB, SI_SLAB_VACUUM, SI_SLAB_PASSIVATED COMMON_PART = { "units": "crystal", @@ -170,3 +172,10 @@ def test_rotate_material(): material.basis.coordinates.values.sort(), rotated_material.basis.coordinates.values.sort() ) assertion_utils.assert_deep_almost_equal(material.lattice, rotated_material.lattice) + + +def test_passivate_surface(): + passivated_material = passivate_surface( + material=Material(SI_SLAB), passivant="H", default_bond_length=1.48, surface="both" + ) + assertion_utils.assert_deep_almost_equal(SI_SLAB_PASSIVATED, passivated_material.to_json()) From b67bc9906094f97854119eba7b78949b2b6b63f6 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:09:34 -0700 Subject: [PATCH 20/89] update: combine coordinate and atom nn functions --- src/py/mat3ra/made/tools/analyze.py | 58 +++++-------------- .../made/tools/build/defect/builders.py | 6 +- 2 files changed, 17 insertions(+), 47 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index a5968357..7632f81c 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -263,9 +263,10 @@ def get_atom_indices_with_condition_on_coordinates( return selected_indices -def get_nearest_neighbors_atom_indices_for_coordinate( +def get_nearest_neighbors_atom_indices( material: Material, coordinate: Optional[List[float]] = None, + tolerance: float = 0.1, cutoff: float = 15.0, ) -> Optional[List[int]]: """ @@ -283,57 +284,26 @@ def get_nearest_neighbors_atom_indices_for_coordinate( coordinate = [0, 0, 0] structure = to_pymatgen(material) voronoi_nn = PymatgenVoronoiNN( - tol=0.5, + tol=tolerance, cutoff=cutoff, - allow_pathological=False, weight="solid_angle", extra_nn_info=False, - compute_adj_neighbors=False, - ) - - structure.append("X", coordinate, validate_proximity=False) - site_index = len(structure.sites) - 1 - neighbors = voronoi_nn.get_nn_info(structure, site_index) - neighboring_atoms_pymatgen_ids = [n["site_index"] for n in neighbors] - structure.remove_sites([-1]) - - all_coordinates = material.basis.coordinates - all_coordinates.filter_by_indices(neighboring_atoms_pymatgen_ids) - return all_coordinates.ids - - -def get_nearest_neighbors_atom_indices_for_atom( - material: Material, - coordinate: Optional[List[float]] = None, - cutoff: float = 15.0, -) -> Optional[List[int]]: - """ - Returns the indices of direct neighboring atoms to a specified position in the material using Voronoi tessellation. - - Args: - material (Material): The material object to find neighbors in. - coordinate (List[float]): The position to find neighbors for. - cutoff (float): The cutoff radius for identifying neighbors. - - Returns: - List[int]: A list of indices of neighboring atoms, or an empty list if no neighbors are found. - """ - if coordinate is None: - coordinate = [0, 0, 0] - structure = to_pymatgen(material) - voronoi_nn = PymatgenVoronoiNN( - tol=0.1, - cutoff=cutoff, - allow_pathological=False, - weight="solid_angle", - extra_nn_info=False, - compute_adj_neighbors=False, + compute_adj_neighbors=True, ) coordinates = material.basis.coordinates coordinates.filter_by_values([coordinate]) - site_index = coordinates.ids[0] + site_index = coordinates.ids[0] if coordinates.ids else None + remove_dummy_atom = False + if site_index is None: + structure.append("X", coordinate, validate_proximity=False) + site_index = len(structure.sites) - 1 + + remove_dummy_atom = True + neighbors = voronoi_nn.get_nn_info(structure, site_index) neighboring_atoms_pymatgen_ids = [n["site_index"] for n in neighbors] + if remove_dummy_atom: + structure.remove_sites([-1]) all_coordinates = material.basis.coordinates all_coordinates.filter_by_indices(neighboring_atoms_pymatgen_ids) diff --git a/src/py/mat3ra/made/tools/build/defect/builders.py b/src/py/mat3ra/made/tools/build/defect/builders.py index 39889ee9..c4eea490 100644 --- a/src/py/mat3ra/made/tools/build/defect/builders.py +++ b/src/py/mat3ra/made/tools/build/defect/builders.py @@ -24,7 +24,7 @@ from ...build import BaseBuilder from ...convert import to_pymatgen from ...analyze import ( - get_nearest_neighbors_atom_indices_for_coordinate, + get_nearest_neighbors_atom_indices, get_atomic_coordinates_extremum, get_closest_site_id_from_coordinate, get_closest_site_id_from_coordinate_and_element, @@ -245,8 +245,8 @@ def get_equidistant_position( coordinate=adatom_coordinate, scaling_factor=scaling_factor, translation_vector=translation_vector ) - neighboring_atoms_ids_in_supercell = get_nearest_neighbors_atom_indices_for_coordinate( - supercell_material, adatom_coordinate_in_supercell + neighboring_atoms_ids_in_supercell = get_nearest_neighbors_atom_indices( + material=supercell_material, coordinate=adatom_coordinate_in_supercell ) if neighboring_atoms_ids_in_supercell is None: raise ValueError("No neighboring atoms found. Try reducing the distance_z.") From 93b670a8697a54718580cfcd06e46bda14fb5c53 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:10:21 -0700 Subject: [PATCH 21/89] chore: run lint fix --- src/py/mat3ra/made/tools/analyze.py | 2 +- tests/py/unit/test_tools_modify.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index 7632f81c..27d7dfab 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -350,7 +350,7 @@ def get_undercoordinated_atoms( atom_neighbors_info = {} for idx in indices_to_check: coordinate = coordinates_array[idx] - neighbors_indices = get_nearest_neighbors_atom_indices_for_atom(material, coordinate, cutoff=6) or [] + neighbors_indices = get_nearest_neighbors_atom_indices(material=material, coordinate=coordinate, cutoff=6) or [] neighbors_coordinates = [coordinates_array[i] for i in neighbors_indices] neighbors_center = get_center_of_coordinates(neighbors_coordinates) atom_neighbors_info[idx] = (len(neighbors_indices), neighbors_indices, neighbors_center) diff --git a/tests/py/unit/test_tools_modify.py b/tests/py/unit/test_tools_modify.py index a4dc76bf..743a3908 100644 --- a/tests/py/unit/test_tools_modify.py +++ b/tests/py/unit/test_tools_modify.py @@ -1,7 +1,6 @@ from ase.build import bulk from mat3ra.made.material import Material from mat3ra.made.tools.convert import from_ase - from mat3ra.made.tools.modify import ( add_vacuum, filter_by_circle_projection, @@ -10,14 +9,14 @@ filter_by_rectangle_projection, filter_by_sphere, filter_by_triangle_projection, + passivate_surface, remove_vacuum, rotate_material, translate_to_z_level, - passivate_surface, ) from mat3ra.utils import assertion as assertion_utils -from .fixtures import SI_CONVENTIONAL_CELL, SI_SLAB, SI_SLAB_VACUUM, SI_SLAB_PASSIVATED +from .fixtures import SI_CONVENTIONAL_CELL, SI_SLAB, SI_SLAB_PASSIVATED, SI_SLAB_VACUUM COMMON_PART = { "units": "crystal", From ceb325f64155d06c7602c273048b898b3943e440 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 22 Aug 2024 20:31:24 -0700 Subject: [PATCH 22/89] update: add passivation for edges --- src/py/mat3ra/made/tools/analyze.py | 2 +- src/py/mat3ra/made/tools/modify.py | 134 +++++++++++++++++++++------- 2 files changed, 103 insertions(+), 33 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index 27d7dfab..c9bb93de 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -267,7 +267,7 @@ def get_nearest_neighbors_atom_indices( material: Material, coordinate: Optional[List[float]] = None, tolerance: float = 0.1, - cutoff: float = 15.0, + cutoff: float = 13.0, ) -> Optional[List[int]]: """ Returns the indices of direct neighboring atoms to a specified position in the material using Voronoi tessellation. diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index 99dfb068..cde078d5 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -536,6 +536,82 @@ class SURFACE_TYPES(str, Enum): BOTH = "both" +def add_passivant_atoms_to_material( + material: Material, + axis: Literal["x", "y", "z"], + surface: Optional[SURFACE_TYPES] = SURFACE_TYPES.BOTH, + tolerance: float = 0.01, + passivant: Optional[str] = "H", + default_bond_length: float = 1.0, +) -> Material: + """ + Add passivant atoms to the specified surface or edge of the material. + + Args: + material (Material): The material object to add passivant atoms to. + axis (AXIS_TYPES): The axis along which the surface or edge lies ("x", "y", or "z"). + surface (SURFACE_TYPES): The surface to add passivant atoms to ("top", "bottom", or "both"). For edges, assumes both. + tolerance (float): The tolerance for selecting surface atoms. + passivant (str): The chemical symbol of the passivating atom (e.g., 'H'). + default_bond_length (float): The default bond length to use if the pair is not found in BOND_LENGTHS_MAP. + + Returns: + Material: The material object with passivation atoms added. + """ + axis_idx = {"x": 0, "y": 1, "z": 2}[axis] + basis = material.basis.copy() + if axis == "z": + if surface in [SURFACE_TYPES.TOP, SURFACE_TYPES.BOTH]: + z_extremum = get_atomic_coordinates_extremum(material, "max", axis) + add_passivant_atoms_to_basis( + material, basis, axis_idx, z_extremum, tolerance, passivant, default_bond_length, positive=True + ) + if surface in [SURFACE_TYPES.BOTTOM, SURFACE_TYPES.BOTH]: + z_extremum = get_atomic_coordinates_extremum(material, "min", axis) + add_passivant_atoms_to_basis( + material, basis, axis_idx, z_extremum, tolerance, passivant, default_bond_length, positive=False + ) + else: + x_or_y_extremum_max = get_atomic_coordinates_extremum(material, "max", axis) + add_passivant_atoms_to_basis( + material, basis, axis_idx, x_or_y_extremum_max, tolerance, passivant, default_bond_length, positive=True + ) + + x_or_y_extremum_min = get_atomic_coordinates_extremum(material, "min", axis) + add_passivant_atoms_to_basis( + material, basis, axis_idx, x_or_y_extremum_min, tolerance, passivant, default_bond_length, positive=False + ) + material.basis = basis + return material + + +def add_passivant_atoms_to_basis( + material, basis, axis_idx, extremum, tolerance, passivant, default_bond_length, positive=True +): + """ + Helper function to add passivant atoms to the specified extremum along a given axis. + """ + min_box = [0, 0, 0] + max_box = [1, 1, 1] + min_box[axis_idx] = extremum - tolerance + max_box[axis_idx] = extremum + tolerance + + surface_atoms = filter_by_box(material, min_box, max_box) + surface_indices = surface_atoms.basis.coordinates.ids + + for idx in surface_indices: + atom_coordinate = material.basis.coordinates.values[idx] + element = material.basis.elements.values[idx] + bond_length = BOND_LENGTHS_MAP.get((element, passivant), default_bond_length) + + bond_length_vector = [0, 0, 0] + bond_length_vector[axis_idx] = bond_length if positive else -bond_length + bond_length_crystal = material.basis.cell.convert_point_to_crystal(bond_length_vector) + + passivant_coordinate = atom_coordinate + bond_length_crystal + basis.add_atom(passivant, passivant_coordinate) + + def passivate_surface( material: Material, passivant: str = "H", @@ -543,7 +619,7 @@ def passivate_surface( surface: SURFACE_TYPES = SURFACE_TYPES.BOTH, ) -> Material: """ - Passivates the top and bottom surfaces of a material by adding atoms along the Z-axis, + Passivates the top and/or bottom surfaces of a material by adding atoms along the Z-axis, with bond lengths determined by the element and passivant. Args: @@ -556,43 +632,37 @@ def passivate_surface( Material: The passivated material. """ material = translate_to_z_level(material, "center") - tolerance = 0.01 - new_basis = material.basis.copy() - if surface in [SURFACE_TYPES.TOP, SURFACE_TYPES.BOTH]: - add_passivant_atoms_to_basis(material, SURFACE_TYPES.TOP, tolerance, passivant, default_bond_length, new_basis) - if surface in [SURFACE_TYPES.BOTTOM, SURFACE_TYPES.BOTH]: - add_passivant_atoms_to_basis( - material, SURFACE_TYPES.BOTTOM, tolerance, passivant, default_bond_length, new_basis - ) - - material.basis = new_basis - return material + return add_passivant_atoms_to_material( + material=material, + axis="z", + surface=surface, + passivant=passivant, + default_bond_length=default_bond_length, + ) -def add_passivant_atoms_to_basis( - material: Material, z_level: SURFACE_TYPES, tolerance: float, passivant: str, default_bond_length: float, basis -) -> None: +def passivate_edges( + material: Material, + passivant: str = "H", + default_bond_length: float = 1.0, + axis: Literal["x", "y"] = "x", +) -> Material: """ - Add passivant atoms to the specified z-level (top or bottom) of the material. + Passivates the edges of a 2D material by adding atoms along the X or Y axis, + with bond lengths determined by the element and passivant. Args: - material (Material): The material object to add passivant atoms to. - z_level (str): The z-level to add passivant atoms to ("top" or "bottom"). - tolerance (float): The tolerance for selecting surface atoms. + material (Material): The material to passivate. passivant (str): The chemical symbol of the passivating atom (e.g., 'H'). default_bond_length (float): The default bond length to use if the pair is not found in BOND_LENGTHS_MAP. - basis: The basis of the new material to which passivant atoms will be added. + axis (AXIS_TYPES): The axis along which the edges lie ("x" or "y"). + + Returns: + Material: The passivated material. """ - z_extremum = get_atomic_coordinates_extremum(material, "max" if z_level == "top" else "min", "z") - surface_atoms = filter_by_box(material, [0, 0, z_extremum - tolerance], [1, 1, z_extremum + tolerance]) - surface_indices = surface_atoms.basis.coordinates.ids - for idx in surface_indices: - atom_coordinate = material.basis.coordinates.values[idx] - element = material.basis.elements.values[idx] - bond_length = BOND_LENGTHS_MAP.get((element, passivant), default_bond_length) - bond_length_crystal = material.basis.cell.convert_point_to_crystal([0, 0, bond_length]) - passivant_coordinate = ( - atom_coordinate + bond_length_crystal if z_level == "top" else atom_coordinate - bond_length_crystal - ) - basis.add_atom(passivant, passivant_coordinate) + material = translate_to_z_level(material, "center") + + return add_passivant_atoms_to_material( + material=material, axis=axis, passivant=passivant, default_bond_length=default_bond_length + ) From 65a7b768b2b485a469d721314e38ff030706f5c4 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 22 Aug 2024 20:34:05 -0700 Subject: [PATCH 23/89] chore: run lint fix --- src/py/mat3ra/made/tools/modify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index cde078d5..7f47374e 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -550,7 +550,7 @@ def add_passivant_atoms_to_material( Args: material (Material): The material object to add passivant atoms to. axis (AXIS_TYPES): The axis along which the surface or edge lies ("x", "y", or "z"). - surface (SURFACE_TYPES): The surface to add passivant atoms to ("top", "bottom", or "both"). For edges, assumes both. + surface (SURFACE_TYPES): The surface to add passivant atoms to ("top", "bottom", or "both"). tolerance (float): The tolerance for selecting surface atoms. passivant (str): The chemical symbol of the passivating atom (e.g., 'H'). default_bond_length (float): The default bond length to use if the pair is not found in BOND_LENGTHS_MAP. @@ -646,7 +646,7 @@ def passivate_edges( material: Material, passivant: str = "H", default_bond_length: float = 1.0, - axis: Literal["x", "y"] = "x", + axis: Literal["x", "y"] = "y", ) -> Material: """ Passivates the edges of a 2D material by adding atoms along the X or Y axis, From 66d96eae9c6226cbb884d4a23cb1742f890965bf Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:07:04 -0700 Subject: [PATCH 24/89] feat: add kd tree surface atoms calculation --- src/py/mat3ra/made/tools/analyze.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index c9bb93de..2b126f45 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -2,6 +2,7 @@ import numpy as np from mat3ra.made.utils import get_center_of_coordinates +from scipy.spatial import cKDTree from ..material import Material from .convert import decorator_convert_material_args_kwargs_to_atoms, to_pymatgen @@ -339,6 +340,33 @@ def get_atomic_coordinates_extremum( return getattr(np, extremum)(values) +def get_surface_atoms_indices(material: Material, distance_threshold: float = 0.2) -> List[int]: + """ + Identify exposed atoms on the top surface of the material. + + Args: + material (Material): Material object to get surface atoms from. + distance_threshold (float): Distance threshold to determine if an atom is considered "covered". + + Returns: + List[int]: List of indices of exposed top surface atoms. + """ + coordinates = np.array(material.basis.coordinates.values) + ids = material.basis.coordinates.ids + kd_tree = cKDTree(coordinates) + z_max = np.max(coordinates[:, 2]) + + exposed_atoms_indices = [] + for idx, (x, y, z) in enumerate(coordinates): + if z >= z_max - distance_threshold: + neighbors_above = kd_tree.query_ball_point([x, y, z + distance_threshold], r=distance_threshold) + + if not any(coordinates[n][2] > z for n in neighbors_above): + exposed_atoms_indices.append(ids[idx]) + + return exposed_atoms_indices + + def get_undercoordinated_atoms( material: Material, indices_to_check: Optional[List[int]] = None, coordination_threshold: Optional[int] = None ) -> Tuple[List[int], dict]: From 64460e9a21febab154959d98dce153c0da9156ca Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:26:23 -0700 Subject: [PATCH 25/89] update: add z factor to look deeper --- src/py/mat3ra/made/tools/analyze.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index 2b126f45..a571eac8 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -340,13 +340,14 @@ def get_atomic_coordinates_extremum( return getattr(np, extremum)(values) -def get_surface_atoms_indices(material: Material, distance_threshold: float = 0.2) -> List[int]: +def get_surface_atoms_indices(material: Material, distance_threshold: float = 0.2, z_factor: int = 10) -> List[int]: """ Identify exposed atoms on the top surface of the material. Args: material (Material): Material object to get surface atoms from. distance_threshold (float): Distance threshold to determine if an atom is considered "covered". + z_factor (int): Factor to multiply distance_threshold when looking deeper into the material. Returns: List[int]: List of indices of exposed top surface atoms. @@ -358,7 +359,7 @@ def get_surface_atoms_indices(material: Material, distance_threshold: float = 0. exposed_atoms_indices = [] for idx, (x, y, z) in enumerate(coordinates): - if z >= z_max - distance_threshold: + if z >= z_max - z_factor * distance_threshold: neighbors_above = kd_tree.query_ball_point([x, y, z + distance_threshold], r=distance_threshold) if not any(coordinates[n][2] > z for n in neighbors_above): From 6169470c88b0e4cd5a994c6d6dc4acf7c041fe9b Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:01:45 -0700 Subject: [PATCH 26/89] update: small adjustment --- src/py/mat3ra/made/tools/analyze.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index a571eac8..34cce89d 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -340,18 +340,19 @@ def get_atomic_coordinates_extremum( return getattr(np, extremum)(values) -def get_surface_atoms_indices(material: Material, distance_threshold: float = 0.2, z_factor: int = 10) -> List[int]: +def get_surface_atoms_indices(material: Material, distance_threshold: float = 2.5, depth: float = 5) -> List[int]: """ Identify exposed atoms on the top surface of the material. Args: material (Material): Material object to get surface atoms from. distance_threshold (float): Distance threshold to determine if an atom is considered "covered". - z_factor (int): Factor to multiply distance_threshold when looking deeper into the material. + depth (float): Depth from the top surface to look for exposed atoms. Returns: List[int]: List of indices of exposed top surface atoms. """ + material.to_cartesian() coordinates = np.array(material.basis.coordinates.values) ids = material.basis.coordinates.ids kd_tree = cKDTree(coordinates) @@ -359,7 +360,7 @@ def get_surface_atoms_indices(material: Material, distance_threshold: float = 0. exposed_atoms_indices = [] for idx, (x, y, z) in enumerate(coordinates): - if z >= z_max - z_factor * distance_threshold: + if z >= z_max - depth: neighbors_above = kd_tree.query_ball_point([x, y, z + distance_threshold], r=distance_threshold) if not any(coordinates[n][2] > z for n in neighbors_above): From a63e32f41e65f3bfef0fe6a07ef3a64f7370dfb5 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:02:10 -0700 Subject: [PATCH 27/89] update :add missing handy function --- src/py/mat3ra/made/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/py/mat3ra/made/utils.py b/src/py/mat3ra/made/utils.py index 15aec478..68c47e25 100644 --- a/src/py/mat3ra/made/utils.py +++ b/src/py/mat3ra/made/utils.py @@ -94,6 +94,9 @@ def to_array_of_values_with_ids(self) -> List[ValueWithId]: def get_element_value_by_index(self, index: int) -> Any: return self.values[index] if index < len(self.values) else None + def get_element_index_by_value(self, value: Any) -> Union[int, None]: + return self.values.index(value) if value in self.values else None + def filter_by_values(self, values: Union[List[Any], Any]): def make_hashable(value): return tuple(value) if isinstance(value, list) else value From 1db1ccd062b541f3aa2d25728bc9501ca5acb125 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:02:46 -0700 Subject: [PATCH 28/89] update: speedup using np --- src/py/mat3ra/made/tools/utils/coordinate.py | 22 ++++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/py/mat3ra/made/tools/utils/coordinate.py b/src/py/mat3ra/made/tools/utils/coordinate.py index 683c54b3..f4533dc2 100644 --- a/src/py/mat3ra/made/tools/utils/coordinate.py +++ b/src/py/mat3ra/made/tools/utils/coordinate.py @@ -20,9 +20,10 @@ def is_coordinate_in_cylinder( Returns: bool: True if the coordinate is inside the cylinder, False otherwise. """ - return (coordinate[0] - center_position[0]) ** 2 + (coordinate[1] - center_position[1]) ** 2 <= radius**2 and ( - min_z <= coordinate[2] <= max_z - ) + np_coordinate = np.array(coordinate) + np_center_position = np.array(center_position) + distance_squared = np.sum((np_coordinate[:2] - np_center_position[:2]) ** 2) + return distance_squared <= radius**2 and min_z <= np_coordinate[2] <= max_z def is_coordinate_in_sphere(coordinate: List[float], center_position: List[float], radius: float = 0.25) -> bool: @@ -42,9 +43,7 @@ def is_coordinate_in_sphere(coordinate: List[float], center_position: List[float return distance_squared <= radius**2 -def is_coordinate_in_box( - coordinate: List[float], min_coordinate: List[float] = [0, 0, 0], max_coordinate: List[float] = [1, 1, 1] -) -> bool: +def is_coordinate_in_box(coordinate: List[float], min_coordinate=None, max_coordinate=None) -> bool: """ Check if a coordinate is inside a box. Args: @@ -54,9 +53,14 @@ def is_coordinate_in_box( Returns: bool: True if the coordinate is inside the box, False otherwise. """ - x_min, y_min, z_min = min_coordinate - x_max, y_max, z_max = max_coordinate - return x_min <= coordinate[0] <= x_max and y_min <= coordinate[1] <= y_max and z_min <= coordinate[2] <= z_max + if max_coordinate is None: + max_coordinate = [1, 1, 1] + if min_coordinate is None: + min_coordinate = [0, 0, 0] + np_coordinate = np.array(coordinate) + np_min_coordinate = np.array(min_coordinate) + np_max_coordinate = np.array(max_coordinate) + return bool(np.all(np_min_coordinate <= np_coordinate) and np.all(np_coordinate <= np_max_coordinate)) def is_coordinate_within_layer( From 3968a148ad9c5a780ca7253e57ce5d505bda3704 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:09:47 -0700 Subject: [PATCH 29/89] fix: get more of the island by selecting deeper --- src/py/mat3ra/made/tools/build/defect/builders.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/defect/builders.py b/src/py/mat3ra/made/tools/build/defect/builders.py index c4eea490..63d35a1c 100644 --- a/src/py/mat3ra/made/tools/build/defect/builders.py +++ b/src/py/mat3ra/made/tools/build/defect/builders.py @@ -392,6 +392,10 @@ class IslandSlabDefectBuilder(SlabDefectBuilder): _ConfigurationType: type(IslandSlabDefectConfiguration) = IslandSlabDefectConfiguration # type: ignore _GeneratedItemType: Material = Material + @staticmethod + def _default_condition(coordinate: List[float]): + return True + def create_island( self, material: Material, @@ -410,27 +414,23 @@ def create_island( Returns: The material with the island added. """ - new_material = material.clone() original_max_z = get_atomic_coordinates_extremum(new_material, use_cartesian_coordinates=False) material_with_additional_layers = self.create_material_with_additional_layers(new_material, thickness) added_layers_max_z = get_atomic_coordinates_extremum(material_with_additional_layers) - if condition is None: + condition = self._default_condition - def condition(coordinate: List[float]): - return True - + thickness_nudge_value = (added_layers_max_z - original_max_z) / thickness atoms_within_island = filter_by_condition_on_coordinates( material=material_with_additional_layers, condition=condition, use_cartesian_coordinates=use_cartesian_coordinates, ) - # Filter atoms in the added layers island_material = filter_by_box( material=atoms_within_island, - min_coordinate=[0, 0, original_max_z], + min_coordinate=[0, 0, original_max_z - thickness_nudge_value], max_coordinate=[1, 1, added_layers_max_z], ) From cdd7f86a7b529e72a50c15cb5188f61e8018470b Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:17:58 -0700 Subject: [PATCH 30/89] fix: adjust test since the island creation fixed --- tests/py/unit/test_tools_build_defect.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/py/unit/test_tools_build_defect.py b/tests/py/unit/test_tools_build_defect.py index 1ee349fa..8ba45e0d 100644 --- a/tests/py/unit/test_tools_build_defect.py +++ b/tests/py/unit/test_tools_build_defect.py @@ -126,8 +126,9 @@ def test_create_island(): defect = create_slab_defect(configuration=island_config, builder=IslandSlabDefectBuilder()) - # Only one atom is in the island for this configuration - assert len(defect.basis.elements.values) == len(SLAB_111.basis.elements.values) + 1 + # Only 2 atoms in the island were added for this configuration + NUMBER_OF_ATOMS_IN_ISLAND = 2 + assert len(defect.basis.elements.values) == len(SLAB_111.basis.elements.values) + NUMBER_OF_ATOMS_IN_ISLAND assert defect.basis.elements.values[-1] == "Si" From 356bad8b1948478d4f5a38d227fec8fcf360f06c Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:25:54 -0700 Subject: [PATCH 31/89] feat: add passivate top function --- src/py/mat3ra/made/tools/modify.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index 7f47374e..ffab5dc9 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -11,6 +11,7 @@ get_atom_indices_within_radius_pbc, get_atomic_coordinates_extremum, get_undercoordinated_atoms, + get_surface_atoms_indices, ) from .convert import from_ase, to_ase from .third_party import ase_add_vacuum @@ -469,6 +470,33 @@ def rotate_material(material: Material, axis: List[int], angle: float) -> Materi return Material(from_ase(atoms)) +def passivate_top(material: Material, passivant: str = "H", bond_length=1.0, depth=10): + """ + Passivate the top surface of the material with the specified element. + + Args: + material (Material): Material object to passivate. + passivant (str): Element to use for passivation. + bond_length (float): Bond length to use for the passivation in Angstroms. + depth (float): Depth from the top surface to passivate in Angstroms. + + Returns: + Material: Material object with the top surface passivated. + """ + new_material = material.clone() + top_atom_indices = get_surface_atoms_indices(material, distance_threshold=2.5, depth=depth) + top_atoms = filter_material_by_ids(material, top_atom_indices) + new_material.to_cartesian() + top_atoms.to_cartesian() + top_atoms_coordinates = top_atoms.basis.coordinates.values + passivant_coordinates = np.array(top_atoms_coordinates) + [0, 0, bond_length] + + for coordinate in passivant_coordinates: + new_material.add_atom(passivant, coordinate) + new_material.to_crystal() + return new_material + + def get_passivant_coordinate_crystal( material: Material, atom_coordinate: np.ndarray, bond_direction: np.ndarray, bond_length: float = 2.0 ): From 88e6ace37c8ddf0fcb6cec23813ab24727cb366e Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:51:18 -0700 Subject: [PATCH 32/89] update: add shadowing parameter --- src/py/mat3ra/made/tools/modify.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index ffab5dc9..f532bb1f 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -470,7 +470,7 @@ def rotate_material(material: Material, axis: List[int], angle: float) -> Materi return Material(from_ase(atoms)) -def passivate_top(material: Material, passivant: str = "H", bond_length=1.0, depth=10): +def passivate_top(material: Material, passivant: str = "H", bond_length=1.0, shadowing_radius: float = 2.5, depth=10): """ Passivate the top surface of the material with the specified element. @@ -478,13 +478,14 @@ def passivate_top(material: Material, passivant: str = "H", bond_length=1.0, dep material (Material): Material object to passivate. passivant (str): Element to use for passivation. bond_length (float): Bond length to use for the passivation in Angstroms. + shadowing_radius (float): Radius for atoms shadowing underlying from passivation in Angstroms. depth (float): Depth from the top surface to passivate in Angstroms. Returns: Material: Material object with the top surface passivated. """ new_material = material.clone() - top_atom_indices = get_surface_atoms_indices(material, distance_threshold=2.5, depth=depth) + top_atom_indices = get_surface_atoms_indices(material, distance_threshold=shadowing_radius, depth=depth) top_atoms = filter_material_by_ids(material, top_atom_indices) new_material.to_cartesian() top_atoms.to_cartesian() From 3719024a10bb048d56ff51af992fb28debf5f458 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 25 Aug 2024 16:45:07 -0700 Subject: [PATCH 33/89] update: optimize --- .../made/tools/build/passivation/__init__.py | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 src/py/mat3ra/made/tools/build/passivation/__init__.py diff --git a/src/py/mat3ra/made/tools/build/passivation/__init__.py b/src/py/mat3ra/made/tools/build/passivation/__init__.py new file mode 100644 index 00000000..53caf477 --- /dev/null +++ b/src/py/mat3ra/made/tools/build/passivation/__init__.py @@ -0,0 +1,197 @@ +def get_passivant_coordinate_crystal( + material: Material, atom_coordinate: np.ndarray, bond_direction: np.ndarray, bond_length: float = 2.0 +): + bond_normal_cartesian = np.array(material.basis.cell.convert_point_to_cartesian(bond_direction.tolist())) + bond_normal_cartesian /= np.linalg.norm(bond_normal_cartesian) + bond_vector_cartesian = bond_normal_cartesian * bond_length + bond_vector_crystal = np.array(material.basis.cell.convert_point_to_crystal(bond_vector_cartesian.tolist())) + passivant_coordinate_crystal = atom_coordinate + bond_vector_crystal + return passivant_coordinate_crystal + + +def passivate_material( + slab: Material, passivant: str, bond_length: float = 2.0, coordination_threshold: Optional[int] = None +): + nudge_value = 0.01 + supercell_scaling_factor = [3, 3, 3] + min_coordinate = [1 / 3, 1 / 3, 1 / 3] + max_coordinate = [2 / 3, 2 / 3, 2 / 3] + adjusted_min_coordinate = (np.array(min_coordinate) - nudge_value).tolist() + adjusted_max_coordinate = (np.array(max_coordinate) + nudge_value).tolist() + slab = translate_to_z_level(slab, "center") + new_basis = slab.basis.copy() + slab_supercell = create_supercell(slab, scaling_factor=supercell_scaling_factor) + + central_cell_ids = [ + id + for id, coordinate in zip(slab_supercell.basis.coordinates.ids, slab_supercell.basis.coordinates.values) + if is_coordinate_in_box(coordinate, adjusted_min_coordinate, adjusted_max_coordinate) + ] + undercoordinated_atom_indices, atom_neighbors_info = get_undercoordinated_atoms( + slab_supercell, central_cell_ids, coordination_threshold + ) + for index in undercoordinated_atom_indices: + atom_coordinate_crystal = np.array(slab_supercell.basis.coordinates.values[index]) + neighbors_average_coordinate_crystal = atom_neighbors_info[index][2] + bond_normal_crystal = np.array(slab_supercell.basis.coordinates.values[index]) - np.array( + neighbors_average_coordinate_crystal + ) + passivant_coordinate_crystal = get_passivant_coordinate_crystal( + slab_supercell, atom_coordinate_crystal, bond_normal_crystal, bond_length + ).tolist() + passivant_coordinate_crystal_original_cell = transform_coordinate_to_supercell( + passivant_coordinate_crystal, + scaling_factor=supercell_scaling_factor, + translation_vector=min_coordinate, + reverse=True, + ) + new_basis.add_atom(passivant, passivant_coordinate_crystal_original_cell) + + slab.basis = new_basis + return slab + + +# TODO: Get this from peroidic table +BOND_LENGTHS_MAP = { + ("C", "H"): 1.09, + ("Ni", "H"): 1.09, + ("Si", "H"): 1.48, +} + + +class SURFACE_TYPES(str, Enum): + TOP = "top" + BOTTOM = "bottom" + BOTH = "both" + + +def add_passivant_atoms_to_material( + material: Material, + axis: Literal["x", "y", "z"], + surface: Optional[SURFACE_TYPES] = SURFACE_TYPES.BOTH, + tolerance: float = 0.01, + passivant: Optional[str] = "H", + default_bond_length: float = 1.0, +) -> Material: + """ + Add passivant atoms to the specified surface or edge of the material. + + Args: + material (Material): The material object to add passivant atoms to. + axis (AXIS_TYPES): The axis along which the surface or edge lies ("x", "y", or "z"). + surface (SURFACE_TYPES): The surface to add passivant atoms to ("top", "bottom", or "both"). + tolerance (float): The tolerance for selecting surface atoms. + passivant (str): The chemical symbol of the passivating atom (e.g., 'H'). + default_bond_length (float): The default bond length to use if the pair is not found in BOND_LENGTHS_MAP. + + Returns: + Material: The material object with passivation atoms added. + """ + axis_idx = {"x": 0, "y": 1, "z": 2}[axis] + basis = material.basis.copy() + if axis == "z": + if surface in [SURFACE_TYPES.TOP, SURFACE_TYPES.BOTH]: + z_extremum = get_atomic_coordinates_extremum(material, "max", axis) + add_passivant_atoms_to_basis( + material, basis, axis_idx, z_extremum, tolerance, passivant, default_bond_length, positive=True + ) + if surface in [SURFACE_TYPES.BOTTOM, SURFACE_TYPES.BOTH]: + z_extremum = get_atomic_coordinates_extremum(material, "min", axis) + add_passivant_atoms_to_basis( + material, basis, axis_idx, z_extremum, tolerance, passivant, default_bond_length, positive=False + ) + else: + x_or_y_extremum_max = get_atomic_coordinates_extremum(material, "max", axis) + add_passivant_atoms_to_basis( + material, basis, axis_idx, x_or_y_extremum_max, tolerance, passivant, default_bond_length, positive=True + ) + + x_or_y_extremum_min = get_atomic_coordinates_extremum(material, "min", axis) + add_passivant_atoms_to_basis( + material, basis, axis_idx, x_or_y_extremum_min, tolerance, passivant, default_bond_length, positive=False + ) + material.basis = basis + return material + + +def add_passivant_atoms_to_basis( + material, basis, axis_idx, extremum, tolerance, passivant, default_bond_length, positive=True +): + """ + Helper function to add passivant atoms to the specified extremum along a given axis. + """ + min_box = [0, 0, 0] + max_box = [1, 1, 1] + min_box[axis_idx] = extremum - tolerance + max_box[axis_idx] = extremum + tolerance + + surface_atoms = filter_by_box(material, min_box, max_box) + surface_indices = surface_atoms.basis.coordinates.ids + + for idx in surface_indices: + atom_coordinate = material.basis.coordinates.values[idx] + element = material.basis.elements.values[idx] + bond_length = BOND_LENGTHS_MAP.get((element, passivant), default_bond_length) + + bond_length_vector = [0, 0, 0] + bond_length_vector[axis_idx] = bond_length if positive else -bond_length + bond_length_crystal = material.basis.cell.convert_point_to_crystal(bond_length_vector) + + passivant_coordinate = atom_coordinate + bond_length_crystal + basis.add_atom(passivant, passivant_coordinate) + + +def passivate_surface( + material: Material, + passivant: str = "H", + default_bond_length: float = 1.0, + surface: SURFACE_TYPES = SURFACE_TYPES.BOTH, +) -> Material: + """ + Passivates the top and/or bottom surfaces of a material by adding atoms along the Z-axis, + with bond lengths determined by the element and passivant. + + Args: + material (Material): The material to passivate. + passivant (str): The chemical symbol of the passivating atom (e.g., 'H'). + default_bond_length (float): The default bond length to use if the pair is not found in BOND_LENGTHS_MAP. + surface (SURFACE_TYPES): The surface to passivate ("top", "bottom", or "both"). + + Returns: + Material: The passivated material. + """ + material = translate_to_z_level(material, "center") + + return add_passivant_atoms_to_material( + material=material, + axis="z", + surface=surface, + passivant=passivant, + default_bond_length=default_bond_length, + ) + + +def passivate_edges( + material: Material, + passivant: str = "H", + default_bond_length: float = 1.0, + axis: Literal["x", "y"] = "y", +) -> Material: + """ + Passivates the edges of a 2D material by adding atoms along the X or Y axis, + with bond lengths determined by the element and passivant. + + Args: + material (Material): The material to passivate. + passivant (str): The chemical symbol of the passivating atom (e.g., 'H'). + default_bond_length (float): The default bond length to use if the pair is not found in BOND_LENGTHS_MAP. + axis (AXIS_TYPES): The axis along which the edges lie ("x" or "y"). + + Returns: + Material: The passivated material. + """ + material = translate_to_z_level(material, "center") + + return add_passivant_atoms_to_material( + material=material, axis=axis, passivant=passivant, default_bond_length=default_bond_length + ) From 8db314a4742ee656bcbeecc5d50b3ca91422b7c6 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 25 Aug 2024 16:45:53 -0700 Subject: [PATCH 34/89] feat: add get id by value --- src/py/mat3ra/made/utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/py/mat3ra/made/utils.py b/src/py/mat3ra/made/utils.py index 15aec478..38ca10c3 100644 --- a/src/py/mat3ra/made/utils.py +++ b/src/py/mat3ra/made/utils.py @@ -94,6 +94,12 @@ def to_array_of_values_with_ids(self) -> List[ValueWithId]: def get_element_value_by_index(self, index: int) -> Any: return self.values[index] if index < len(self.values) else None + def get_element_id_by_value(self, value: Any) -> Optional[int]: + try: + return self.ids[self.values.index(value)] + except ValueError: + return None + def filter_by_values(self, values: Union[List[Any], Any]): def make_hashable(value): return tuple(value) if isinstance(value, list) else value From e7ae0c4883c8f4ae490cbb02d0e2db5d24e70f14 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 25 Aug 2024 16:46:32 -0700 Subject: [PATCH 35/89] update: optimize search --- src/py/mat3ra/made/tools/analyze.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index c9bb93de..547cc008 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -291,8 +291,7 @@ def get_nearest_neighbors_atom_indices( compute_adj_neighbors=True, ) coordinates = material.basis.coordinates - coordinates.filter_by_values([coordinate]) - site_index = coordinates.ids[0] if coordinates.ids else None + site_index = coordinates.get_element_id_by_value(coordinate) remove_dummy_atom = False if site_index is None: structure.append("X", coordinate, validate_proximity=False) From 97563e07cf13ad765d93783332c141023023bd4c Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 25 Aug 2024 16:48:08 -0700 Subject: [PATCH 36/89] update: add structure for passivation builders --- .../made/tools/build/passivation/__init__.py | 14 -------------- .../made/tools/build/passivation/builders.py | 0 .../made/tools/build/passivation/configuration.py | 0 .../mat3ra/made/tools/build/passivation/enums.py | 14 ++++++++++++++ 4 files changed, 14 insertions(+), 14 deletions(-) create mode 100644 src/py/mat3ra/made/tools/build/passivation/builders.py create mode 100644 src/py/mat3ra/made/tools/build/passivation/configuration.py create mode 100644 src/py/mat3ra/made/tools/build/passivation/enums.py diff --git a/src/py/mat3ra/made/tools/build/passivation/__init__.py b/src/py/mat3ra/made/tools/build/passivation/__init__.py index 53caf477..85277154 100644 --- a/src/py/mat3ra/made/tools/build/passivation/__init__.py +++ b/src/py/mat3ra/made/tools/build/passivation/__init__.py @@ -51,20 +51,6 @@ def passivate_material( return slab -# TODO: Get this from peroidic table -BOND_LENGTHS_MAP = { - ("C", "H"): 1.09, - ("Ni", "H"): 1.09, - ("Si", "H"): 1.48, -} - - -class SURFACE_TYPES(str, Enum): - TOP = "top" - BOTTOM = "bottom" - BOTH = "both" - - def add_passivant_atoms_to_material( material: Material, axis: Literal["x", "y", "z"], diff --git a/src/py/mat3ra/made/tools/build/passivation/builders.py b/src/py/mat3ra/made/tools/build/passivation/builders.py new file mode 100644 index 00000000..e69de29b diff --git a/src/py/mat3ra/made/tools/build/passivation/configuration.py b/src/py/mat3ra/made/tools/build/passivation/configuration.py new file mode 100644 index 00000000..e69de29b diff --git a/src/py/mat3ra/made/tools/build/passivation/enums.py b/src/py/mat3ra/made/tools/build/passivation/enums.py new file mode 100644 index 00000000..fc3e7cc6 --- /dev/null +++ b/src/py/mat3ra/made/tools/build/passivation/enums.py @@ -0,0 +1,14 @@ +# TODO: Get this from peroidic table +from enum import Enum + +BOND_LENGTHS_MAP = { + ("C", "H"): 1.09, + ("Ni", "H"): 1.09, + ("Si", "H"): 1.48, +} + + +class SURFACE_TYPES(str, Enum): + TOP = "top" + BOTTOM = "bottom" + BOTH = "both" From af7b1b1ca9cad834e637b3721cb236966f3a1fcf Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 25 Aug 2024 16:48:40 -0700 Subject: [PATCH 37/89] update: move passivation functions --- src/py/mat3ra/made/tools/modify.py | 199 ----------------------------- 1 file changed, 199 deletions(-) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index 7f47374e..b4bc278a 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -467,202 +467,3 @@ def rotate_material(material: Material, axis: List[int], angle: float) -> Materi atoms.wrap() return Material(from_ase(atoms)) - - -def get_passivant_coordinate_crystal( - material: Material, atom_coordinate: np.ndarray, bond_direction: np.ndarray, bond_length: float = 2.0 -): - bond_normal_cartesian = np.array(material.basis.cell.convert_point_to_cartesian(bond_direction.tolist())) - bond_normal_cartesian /= np.linalg.norm(bond_normal_cartesian) - bond_vector_cartesian = bond_normal_cartesian * bond_length - bond_vector_crystal = np.array(material.basis.cell.convert_point_to_crystal(bond_vector_cartesian.tolist())) - passivant_coordinate_crystal = atom_coordinate + bond_vector_crystal - return passivant_coordinate_crystal - - -def passivate_material( - slab: Material, passivant: str, bond_length: float = 2.0, coordination_threshold: Optional[int] = None -): - nudge_value = 0.01 - supercell_scaling_factor = [3, 3, 3] - min_coordinate = [1 / 3, 1 / 3, 1 / 3] - max_coordinate = [2 / 3, 2 / 3, 2 / 3] - adjusted_min_coordinate = (np.array(min_coordinate) - nudge_value).tolist() - adjusted_max_coordinate = (np.array(max_coordinate) + nudge_value).tolist() - slab = translate_to_z_level(slab, "center") - new_basis = slab.basis.copy() - slab_supercell = create_supercell(slab, scaling_factor=supercell_scaling_factor) - - central_cell_ids = [ - id - for id, coordinate in zip(slab_supercell.basis.coordinates.ids, slab_supercell.basis.coordinates.values) - if is_coordinate_in_box(coordinate, adjusted_min_coordinate, adjusted_max_coordinate) - ] - undercoordinated_atom_indices, atom_neighbors_info = get_undercoordinated_atoms( - slab_supercell, central_cell_ids, coordination_threshold - ) - for index in undercoordinated_atom_indices: - atom_coordinate_crystal = np.array(slab_supercell.basis.coordinates.values[index]) - neighbors_average_coordinate_crystal = atom_neighbors_info[index][2] - bond_normal_crystal = np.array(slab_supercell.basis.coordinates.values[index]) - np.array( - neighbors_average_coordinate_crystal - ) - passivant_coordinate_crystal = get_passivant_coordinate_crystal( - slab_supercell, atom_coordinate_crystal, bond_normal_crystal, bond_length - ).tolist() - passivant_coordinate_crystal_original_cell = transform_coordinate_to_supercell( - passivant_coordinate_crystal, - scaling_factor=supercell_scaling_factor, - translation_vector=min_coordinate, - reverse=True, - ) - new_basis.add_atom(passivant, passivant_coordinate_crystal_original_cell) - - slab.basis = new_basis - return slab - - -# TODO: Get this from peroidic table -BOND_LENGTHS_MAP = { - ("C", "H"): 1.09, - ("Ni", "H"): 1.09, - ("Si", "H"): 1.48, -} - - -class SURFACE_TYPES(str, Enum): - TOP = "top" - BOTTOM = "bottom" - BOTH = "both" - - -def add_passivant_atoms_to_material( - material: Material, - axis: Literal["x", "y", "z"], - surface: Optional[SURFACE_TYPES] = SURFACE_TYPES.BOTH, - tolerance: float = 0.01, - passivant: Optional[str] = "H", - default_bond_length: float = 1.0, -) -> Material: - """ - Add passivant atoms to the specified surface or edge of the material. - - Args: - material (Material): The material object to add passivant atoms to. - axis (AXIS_TYPES): The axis along which the surface or edge lies ("x", "y", or "z"). - surface (SURFACE_TYPES): The surface to add passivant atoms to ("top", "bottom", or "both"). - tolerance (float): The tolerance for selecting surface atoms. - passivant (str): The chemical symbol of the passivating atom (e.g., 'H'). - default_bond_length (float): The default bond length to use if the pair is not found in BOND_LENGTHS_MAP. - - Returns: - Material: The material object with passivation atoms added. - """ - axis_idx = {"x": 0, "y": 1, "z": 2}[axis] - basis = material.basis.copy() - if axis == "z": - if surface in [SURFACE_TYPES.TOP, SURFACE_TYPES.BOTH]: - z_extremum = get_atomic_coordinates_extremum(material, "max", axis) - add_passivant_atoms_to_basis( - material, basis, axis_idx, z_extremum, tolerance, passivant, default_bond_length, positive=True - ) - if surface in [SURFACE_TYPES.BOTTOM, SURFACE_TYPES.BOTH]: - z_extremum = get_atomic_coordinates_extremum(material, "min", axis) - add_passivant_atoms_to_basis( - material, basis, axis_idx, z_extremum, tolerance, passivant, default_bond_length, positive=False - ) - else: - x_or_y_extremum_max = get_atomic_coordinates_extremum(material, "max", axis) - add_passivant_atoms_to_basis( - material, basis, axis_idx, x_or_y_extremum_max, tolerance, passivant, default_bond_length, positive=True - ) - - x_or_y_extremum_min = get_atomic_coordinates_extremum(material, "min", axis) - add_passivant_atoms_to_basis( - material, basis, axis_idx, x_or_y_extremum_min, tolerance, passivant, default_bond_length, positive=False - ) - material.basis = basis - return material - - -def add_passivant_atoms_to_basis( - material, basis, axis_idx, extremum, tolerance, passivant, default_bond_length, positive=True -): - """ - Helper function to add passivant atoms to the specified extremum along a given axis. - """ - min_box = [0, 0, 0] - max_box = [1, 1, 1] - min_box[axis_idx] = extremum - tolerance - max_box[axis_idx] = extremum + tolerance - - surface_atoms = filter_by_box(material, min_box, max_box) - surface_indices = surface_atoms.basis.coordinates.ids - - for idx in surface_indices: - atom_coordinate = material.basis.coordinates.values[idx] - element = material.basis.elements.values[idx] - bond_length = BOND_LENGTHS_MAP.get((element, passivant), default_bond_length) - - bond_length_vector = [0, 0, 0] - bond_length_vector[axis_idx] = bond_length if positive else -bond_length - bond_length_crystal = material.basis.cell.convert_point_to_crystal(bond_length_vector) - - passivant_coordinate = atom_coordinate + bond_length_crystal - basis.add_atom(passivant, passivant_coordinate) - - -def passivate_surface( - material: Material, - passivant: str = "H", - default_bond_length: float = 1.0, - surface: SURFACE_TYPES = SURFACE_TYPES.BOTH, -) -> Material: - """ - Passivates the top and/or bottom surfaces of a material by adding atoms along the Z-axis, - with bond lengths determined by the element and passivant. - - Args: - material (Material): The material to passivate. - passivant (str): The chemical symbol of the passivating atom (e.g., 'H'). - default_bond_length (float): The default bond length to use if the pair is not found in BOND_LENGTHS_MAP. - surface (SURFACE_TYPES): The surface to passivate ("top", "bottom", or "both"). - - Returns: - Material: The passivated material. - """ - material = translate_to_z_level(material, "center") - - return add_passivant_atoms_to_material( - material=material, - axis="z", - surface=surface, - passivant=passivant, - default_bond_length=default_bond_length, - ) - - -def passivate_edges( - material: Material, - passivant: str = "H", - default_bond_length: float = 1.0, - axis: Literal["x", "y"] = "y", -) -> Material: - """ - Passivates the edges of a 2D material by adding atoms along the X or Y axis, - with bond lengths determined by the element and passivant. - - Args: - material (Material): The material to passivate. - passivant (str): The chemical symbol of the passivating atom (e.g., 'H'). - default_bond_length (float): The default bond length to use if the pair is not found in BOND_LENGTHS_MAP. - axis (AXIS_TYPES): The axis along which the edges lie ("x" or "y"). - - Returns: - Material: The passivated material. - """ - material = translate_to_z_level(material, "center") - - return add_passivant_atoms_to_material( - material=material, axis=axis, passivant=passivant, default_bond_length=default_bond_length - ) From b05bf5b00881f1f1287de527cb9572ebc0b8ba69 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 10:39:30 -0700 Subject: [PATCH 38/89] update: move passivation functions --- src/py/mat3ra/made/tools/modify.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index b4bc278a..f43bc242 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -1,16 +1,11 @@ -from enum import Enum from typing import Callable, List, Literal, Optional, Union -import numpy as np from mat3ra.made.material import Material -from mat3ra.made.tools.build.supercell import create_supercell -from mat3ra.made.tools.utils import transform_coordinate_to_supercell from .analyze import ( get_atom_indices_with_condition_on_coordinates, get_atom_indices_within_radius_pbc, get_atomic_coordinates_extremum, - get_undercoordinated_atoms, ) from .convert import from_ase, to_ase from .third_party import ase_add_vacuum From 015f712674ebde0bf2ea58f4694504b49ddd585e Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 10:40:19 -0700 Subject: [PATCH 39/89] chore: add types --- src/py/mat3ra/made/tools/build/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/__init__.py b/src/py/mat3ra/made/tools/build/__init__.py index a6341f0a..e58f6617 100644 --- a/src/py/mat3ra/made/tools/build/__init__.py +++ b/src/py/mat3ra/made/tools/build/__init__.py @@ -117,11 +117,11 @@ def get_material( ) -> Material: return self.get_materials(configuration, selector_parameters, post_process_parameters)[0] - def _update_material_name(self, material, configuration): + def _update_material_name(self, material, configuration) -> Material: # Do nothing by default return material - def _update_material_metadata(self, material, configuration): + def _update_material_metadata(self, material, configuration) -> Material: if "build" not in material.metadata: material.metadata["build"] = {} material.metadata["build"]["configuration"] = configuration.to_json() From 611ecfc233ad586860e9fdbafd0ec47bc8456a58 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 10:42:26 -0700 Subject: [PATCH 40/89] update: move passivation tests --- tests/py/unit/test_tools_build_passivation.py | 15 +++++++++++++++ tests/py/unit/test_tools_modify.py | 8 -------- 2 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 tests/py/unit/test_tools_build_passivation.py diff --git a/tests/py/unit/test_tools_build_passivation.py b/tests/py/unit/test_tools_build_passivation.py new file mode 100644 index 00000000..fd856882 --- /dev/null +++ b/tests/py/unit/test_tools_build_passivation.py @@ -0,0 +1,15 @@ +from mat3ra.made.material import Material + +from mat3ra.made.tools.modify import ( + passivate_surface, +) +from mat3ra.utils import assertion as assertion_utils + +from .fixtures import SI_SLAB, SI_SLAB_PASSIVATED + + +def test_passivate_surface(): + passivated_material = passivate_surface( + material=Material(SI_SLAB), passivant="H", default_bond_length=1.48, surface="both" + ) + assertion_utils.assert_deep_almost_equal(SI_SLAB_PASSIVATED, passivated_material.to_json()) diff --git a/tests/py/unit/test_tools_modify.py b/tests/py/unit/test_tools_modify.py index 743a3908..8960f023 100644 --- a/tests/py/unit/test_tools_modify.py +++ b/tests/py/unit/test_tools_modify.py @@ -9,7 +9,6 @@ filter_by_rectangle_projection, filter_by_sphere, filter_by_triangle_projection, - passivate_surface, remove_vacuum, rotate_material, translate_to_z_level, @@ -171,10 +170,3 @@ def test_rotate_material(): material.basis.coordinates.values.sort(), rotated_material.basis.coordinates.values.sort() ) assertion_utils.assert_deep_almost_equal(material.lattice, rotated_material.lattice) - - -def test_passivate_surface(): - passivated_material = passivate_surface( - material=Material(SI_SLAB), passivant="H", default_bond_length=1.48, surface="both" - ) - assertion_utils.assert_deep_almost_equal(SI_SLAB_PASSIVATED, passivated_material.to_json()) From 69cdc0bf8af8f01e3bc8b5eea76458bc1daaf309 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 10:46:03 -0700 Subject: [PATCH 41/89] update: add enums --- src/py/mat3ra/made/tools/build/passivation/enums.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/py/mat3ra/made/tools/build/passivation/enums.py b/src/py/mat3ra/made/tools/build/passivation/enums.py index fc3e7cc6..53a85c23 100644 --- a/src/py/mat3ra/made/tools/build/passivation/enums.py +++ b/src/py/mat3ra/made/tools/build/passivation/enums.py @@ -8,7 +8,13 @@ } -class SURFACE_TYPES(str, Enum): +class SurfaceTypes(str, Enum): TOP = "top" BOTTOM = "bottom" BOTH = "both" + + +class EdgeTypes(str, Enum): + ALONG_A = "along_a" + ALONG_B = "along_b" + BOTH = "both" From ab9f2dcd4469137465ec51f135a031ba51d4c30d Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 10:46:22 -0700 Subject: [PATCH 42/89] feat: add surface and edge configs --- .../tools/build/passivation/configuration.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/py/mat3ra/made/tools/build/passivation/configuration.py b/src/py/mat3ra/made/tools/build/passivation/configuration.py index e69de29b..b8a5837d 100644 --- a/src/py/mat3ra/made/tools/build/passivation/configuration.py +++ b/src/py/mat3ra/made/tools/build/passivation/configuration.py @@ -0,0 +1,44 @@ +from typing import Optional + +from mat3ra.made.material import Material +from ...build import BaseConfiguration + + +class PassivationConfiguration(BaseConfiguration): + """ + Configuration for a passivation. + + Args: + slab (Material): The Material object. + passivant (str): The passivant element. + bond_length (float): The bond length. + """ + + slab: Material + passivant: str = "H" + bond_length: float = 1.0 + + @property + def _json(self): + return { + "type": self.get_cls_name(), + "slab": self.slab.to_json(), + "passivant": self.passivant, + "bond_length": self.bond_length, + } + + +class SurfacePassivationConfiguration(PassivationConfiguration): + """ + Configuration for a passivation. + """ + + surface: SurfaceTypes = SurfaceTypes.BOTH + + +class EdgePassivationConfiguration(PassivationConfiguration): + """ + Configuration for a passivation. + """ + + edge: EdgeTypes = EdgeTypes.BOTH From 589ba022f89699701618581ac2100505985bfef5 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 10:46:43 -0700 Subject: [PATCH 43/89] updaet: wip on passivation builders --- .../made/tools/build/passivation/builders.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/py/mat3ra/made/tools/build/passivation/builders.py b/src/py/mat3ra/made/tools/build/passivation/builders.py index e69de29b..d6790a72 100644 --- a/src/py/mat3ra/made/tools/build/passivation/builders.py +++ b/src/py/mat3ra/made/tools/build/passivation/builders.py @@ -0,0 +1,68 @@ +from typing import List +from ...build import BaseBuilder +from mat3ra.made.material import Material +from .configuration import PassivationConfiguration + + +class PassivationBuilder(BaseBuilder): + """ + Base class for passivation builders. + """ + + def _generate(self, configuration: PassivationConfiguration) -> List[Material]: + return [self.create_passivated_material(configuration)] + + def _update_material_name(self, material, configuration): + material = super()._update_material_name(material, configuration) + material.name += f" {configuration.passivant}-passivated" + return material + + def _update_material_basis(self, material, configuration): + return super()._update_material_basis(material, configuration) + + def create_passivated_material(self, configuration: PassivationConfiguration) -> Material: + raise NotImplementedError + + def _add_passivant_atoms(self, material: Material, coordinates: list, passivant: str) -> Material: + """ + Add passivant atoms to the provided coordinates in the material. + + Args: + material (Material): The material object to add passivant atoms to. + coordinates (list): The coordinates to add passivant atoms to. + passivant (str): The chemical symbol of the passivating atom (e.g., 'H'). + + Returns: + Material: The material object with passivation atoms added. + """ + for coord in coordinates: + material.add_atom(passivant, coord) + return material + + +class SurfacePassivationBuilder(PassivationBuilder): + """ + Builder for passivating a surface. + + Detects surface atoms lokking along Z axis and passivates either the top or bottom surface or both. + """ + + def create_passivated_material(self, configuration: PassivationConfiguration) -> Material: + # Reference to the passivate_surface function + # startLine: 56 + # endLine: 99 + pass + + +class EdgePassivationBuilder(PassivationBuilder): + """ + Builder for passivating an edge. + + Detects edge atoms looking perpendicular to the Z axis and passivates them. + """ + + def create_passivated_material(self, configuration: PassivationConfiguration) -> Material: + # Reference to the passivate_edges function + # startLine: 100 + # endLine: 143 + pass From acc6da32c01639c071c1be20aac216410ef3e9ea Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 11:22:04 -0700 Subject: [PATCH 44/89] update: adjust analyze --- src/py/mat3ra/made/tools/analyze.py | 58 +++++++++++------------------ 1 file changed, 22 insertions(+), 36 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index 9ce8937a..0119c588 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -4,6 +4,7 @@ from mat3ra.made.utils import get_center_of_coordinates from scipy.spatial import cKDTree +from .build.passivation.enums import SurfaceTypes from ..material import Material from .convert import decorator_convert_material_args_kwargs_to_atoms, to_pymatgen from .third_party import ASEAtoms, PymatgenIStructure, PymatgenVoronoiNN @@ -339,57 +340,42 @@ def get_atomic_coordinates_extremum( return getattr(np, extremum)(values) -def get_surface_atoms_indices(material: Material, distance_threshold: float = 2.5, depth: float = 5) -> List[int]: +def get_surface_atoms_indices( + material: Material, surface: SurfaceTypes = SurfaceTypes.TOP, distance_threshold: float = 2.5, depth: float = 5 +) -> List[int]: """ - Identify exposed atoms on the top surface of the material. + Identify exposed atoms on the top or bottom surface of the material. Args: material (Material): Material object to get surface atoms from. + surface (SurfaceTypes): Specify "top" or "bottom" to detect the respective surface atoms. distance_threshold (float): Distance threshold to determine if an atom is considered "covered". - depth (float): Depth from the top surface to look for exposed atoms. + depth (float): Depth from the surface to look for exposed atoms. Returns: - List[int]: List of indices of exposed top surface atoms. + List[int]: List of indices of exposed surface atoms. """ material.to_cartesian() coordinates = np.array(material.basis.coordinates.values) ids = material.basis.coordinates.ids kd_tree = cKDTree(coordinates) - z_max = np.max(coordinates[:, 2]) + + if surface == SurfaceTypes.TOP: + z_extremum = np.max(coordinates[:, 2]) + compare = lambda z: z >= z_extremum - depth + neighbor_check = lambda z, neighbors: not any(coordinates[n][2] > z for n in neighbors) + elif surface == SurfaceTypes.BOTTOM: + z_extremum = np.min(coordinates[:, 2]) + compare = lambda z: z <= z_extremum + depth + neighbor_check = lambda z, neighbors: not any(coordinates[n][2] < z for n in neighbors) + else: + raise ValueError(f"Surface must be {SurfaceTypes.TOP} or {SurfaceTypes.BOTTOM}") exposed_atoms_indices = [] for idx, (x, y, z) in enumerate(coordinates): - if z >= z_max - depth: - neighbors_above = kd_tree.query_ball_point([x, y, z + distance_threshold], r=distance_threshold) - - if not any(coordinates[n][2] > z for n in neighbors_above): + if compare(z): + neighbors = kd_tree.query_ball_point([x, y, z + distance_threshold], r=distance_threshold) + if neighbor_check(z, neighbors): exposed_atoms_indices.append(ids[idx]) return exposed_atoms_indices - - -def get_undercoordinated_atoms( - material: Material, indices_to_check: Optional[List[int]] = None, coordination_threshold: Optional[int] = None -) -> Tuple[List[int], dict]: - if indices_to_check is None: - indices_to_check = material.basis.coordinates.ids - - coordinates_array = material.basis.coordinates.values - number_of_neighbors = [] - atom_neighbors_info = {} - for idx in indices_to_check: - coordinate = coordinates_array[idx] - neighbors_indices = get_nearest_neighbors_atom_indices(material=material, coordinate=coordinate, cutoff=6) or [] - neighbors_coordinates = [coordinates_array[i] for i in neighbors_indices] - neighbors_center = get_center_of_coordinates(neighbors_coordinates) - atom_neighbors_info[idx] = (len(neighbors_indices), neighbors_indices, neighbors_center) - number_of_neighbors.append(len(neighbors_indices)) - - if coordination_threshold is None: - coordination_threshold = np.max(number_of_neighbors) - undercoordinated_atom_indices = [ - idx - for idx, num_neighbors in zip(indices_to_check, number_of_neighbors) - if num_neighbors < coordination_threshold - ] - return undercoordinated_atom_indices, atom_neighbors_info From 5a90895fde022644d82ad402690fd5c6f21adcc8 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 12:13:31 -0700 Subject: [PATCH 45/89] chore: small rename --- src/py/mat3ra/made/tools/analyze.py | 6 +++--- src/py/mat3ra/made/tools/modify.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index 0119c588..24d0e6e3 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -341,7 +341,7 @@ def get_atomic_coordinates_extremum( def get_surface_atoms_indices( - material: Material, surface: SurfaceTypes = SurfaceTypes.TOP, distance_threshold: float = 2.5, depth: float = 5 + material: Material, surface: SurfaceTypes = SurfaceTypes.TOP, shadowing_radius: float = 2.5, depth: float = 5 ) -> List[int]: """ Identify exposed atoms on the top or bottom surface of the material. @@ -349,7 +349,7 @@ def get_surface_atoms_indices( Args: material (Material): Material object to get surface atoms from. surface (SurfaceTypes): Specify "top" or "bottom" to detect the respective surface atoms. - distance_threshold (float): Distance threshold to determine if an atom is considered "covered". + shadowing_radius (float): Radius for atoms shadowing underlying from detecting as exposed. depth (float): Depth from the surface to look for exposed atoms. Returns: @@ -374,7 +374,7 @@ def get_surface_atoms_indices( exposed_atoms_indices = [] for idx, (x, y, z) in enumerate(coordinates): if compare(z): - neighbors = kd_tree.query_ball_point([x, y, z + distance_threshold], r=distance_threshold) + neighbors = kd_tree.query_ball_point([x, y, z + shadowing_radius], r=shadowing_radius) if neighbor_check(z, neighbors): exposed_atoms_indices.append(ids[idx]) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index 712cce51..eef5ad52 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -481,7 +481,7 @@ def passivate_top(material: Material, passivant: str = "H", bond_length=1.0, sha Material: Material object with the top surface passivated. """ new_material = material.clone() - top_atom_indices = get_surface_atoms_indices(material, distance_threshold=shadowing_radius, depth=depth) + top_atom_indices = get_surface_atoms_indices(material, shadowing_radius=shadowing_radius, depth=depth) top_atoms = filter_material_by_ids(material, top_atom_indices) new_material.to_cartesian() top_atoms.to_cartesian() @@ -492,4 +492,3 @@ def passivate_top(material: Material, passivant: str = "H", bond_length=1.0, sha new_material.add_atom(passivant, coordinate) new_material.to_crystal() return new_material - From e931db504d6ba26680344ba8ffd948c340c5901e Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 13:30:42 -0700 Subject: [PATCH 46/89] update: create surface passivation --- .../made/tools/build/passivation/builders.py | 94 +++++++++++++++---- 1 file changed, 75 insertions(+), 19 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/passivation/builders.py b/src/py/mat3ra/made/tools/build/passivation/builders.py index d6790a72..4741fe24 100644 --- a/src/py/mat3ra/made/tools/build/passivation/builders.py +++ b/src/py/mat3ra/made/tools/build/passivation/builders.py @@ -1,7 +1,14 @@ from typing import List -from ...build import BaseBuilder + +import numpy as np from mat3ra.made.material import Material -from .configuration import PassivationConfiguration +from pydantic import BaseModel + +from .enums import SurfaceTypes +from ...analyze import get_surface_atoms_indices +from ...modify import translate_to_z_level +from ...build import BaseBuilder +from .configuration import PassivationConfiguration, SurfacePassivationConfiguration, EdgePassivationConfiguration class PassivationBuilder(BaseBuilder): @@ -9,19 +16,22 @@ class PassivationBuilder(BaseBuilder): Base class for passivation builders. """ - def _generate(self, configuration: PassivationConfiguration) -> List[Material]: + _GeneratedItemType = Material + _ConfigurationType = PassivationConfiguration + + def _generate(self, configuration: BaseBuilder._ConfigurationType) -> List[Material]: return [self.create_passivated_material(configuration)] - def _update_material_name(self, material, configuration): + def _update_material_name( + self, material: BaseBuilder._GeneratedItemType, configuration: BaseBuilder._ConfigurationType + ) -> BaseBuilder._GeneratedItemType: material = super()._update_material_name(material, configuration) material.name += f" {configuration.passivant}-passivated" return material - def _update_material_basis(self, material, configuration): - return super()._update_material_basis(material, configuration) - - def create_passivated_material(self, configuration: PassivationConfiguration) -> Material: - raise NotImplementedError + def create_passivated_material(self, configuration: BaseBuilder._ConfigurationType) -> Material: + material = translate_to_z_level(configuration.slab, "center") + return material def _add_passivant_atoms(self, material: Material, coordinates: list, passivant: str) -> Material: """ @@ -40,18 +50,67 @@ def _add_passivant_atoms(self, material: Material, coordinates: list, passivant: return material +class SurfacePassivationBuilderParameters(BaseModel): + """ + Parameters for the SurfacePassivationBuilder. + + Args: + shadowing_radius (float): Radius for atoms shadowing underlying from passivation, in Angstroms. + depth (float): Depth from the top to look for exposed surface atoms to passivate, in Angstroms. + """ + + shadowing_radius: float = 2.5 + depth: float = 5.0 + + class SurfacePassivationBuilder(PassivationBuilder): """ Builder for passivating a surface. - Detects surface atoms lokking along Z axis and passivates either the top or bottom surface or both. + Detects surface atoms looking along Z axis and passivates either the top or bottom surface or both. """ - def create_passivated_material(self, configuration: PassivationConfiguration) -> Material: - # Reference to the passivate_surface function - # startLine: 56 - # endLine: 99 - pass + def create_passivated_material(self, configuration: SurfacePassivationConfiguration) -> Material: + material = super().create_passivated_material(configuration) + passivant_coordinates_values_top = np.array([]) + passivant_coordinates_values_bottom = np.array([]) + + if configuration.surface == SurfaceTypes.TOP or configuration.surface == SurfaceTypes.BOTH: + passivant_coordinates_values_top = self._get_passivant_coordinates( + material, + SurfaceTypes.TOP, + configuration.bond_length, + self.build_parameters.shadowing_radius, + self.build_parameters.depth, + ) + + if configuration.surface == SurfaceTypes.BOTTOM or configuration.surface == SurfaceTypes.BOTH: + passivant_coordinates_values_bottom = self._get_passivant_coordinates( + material, + SurfaceTypes.BOTTOM, + configuration.bond_length, + self.build_parameters.shadowing_radius, + self.build_parameters.depth, + ) + + passivant_coordinates_values = ( + passivant_coordinates_values_bottom.tolist() + passivant_coordinates_values_top.tolist() + ) + return self._add_passivant_atoms(material, passivant_coordinates_values, configuration.passivant) + + def _get_passivant_coordinates(self, material, surface, bond_length, shadowing_radius, depth): + surface_atoms_indices = get_surface_atoms_indices( + material=material, + surface=surface, + shadowing_radius=shadowing_radius, + depth=depth, + ) + surface_atoms_coordinates = [ + material.basis.coordinates.get_element_value_by_index(i) for i in surface_atoms_indices + ] + bond_vector = [0, 0, bond_length] if surface == SurfaceTypes.TOP else [0, 0, -bond_length] + passivant_bond_vector_crystal = material.basis.cell.convert_point_to_crystal(bond_vector) + return np.array(surface_atoms_coordinates) + np.array(passivant_bond_vector_crystal) class EdgePassivationBuilder(PassivationBuilder): @@ -61,8 +120,5 @@ class EdgePassivationBuilder(PassivationBuilder): Detects edge atoms looking perpendicular to the Z axis and passivates them. """ - def create_passivated_material(self, configuration: PassivationConfiguration) -> Material: - # Reference to the passivate_edges function - # startLine: 100 - # endLine: 143 + def create_passivated_material(self, configuration: EdgePassivationConfiguration) -> Material: pass From eccb8618dfc21d42e8e26720f0603ea4a7a3bb3b Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 13:51:42 -0700 Subject: [PATCH 47/89] update: correctly detect surface --- src/py/mat3ra/made/tools/analyze.py | 33 ++++++++++++----------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index 24d0e6e3..51823e8c 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -1,11 +1,10 @@ -from typing import Callable, List, Literal, Optional, Tuple +from typing import Callable, List, Literal, Optional import numpy as np -from mat3ra.made.utils import get_center_of_coordinates from scipy.spatial import cKDTree -from .build.passivation.enums import SurfaceTypes from ..material import Material +from .build.passivation.enums import SurfaceTypes from .convert import decorator_convert_material_args_kwargs_to_atoms, to_pymatgen from .third_party import ASEAtoms, PymatgenIStructure, PymatgenVoronoiNN @@ -355,27 +354,23 @@ def get_surface_atoms_indices( Returns: List[int]: List of indices of exposed surface atoms. """ - material.to_cartesian() - coordinates = np.array(material.basis.coordinates.values) - ids = material.basis.coordinates.ids + new_material = material.clone() + new_material.to_cartesian() + coordinates = np.array(new_material.basis.coordinates.values) + ids = new_material.basis.coordinates.ids kd_tree = cKDTree(coordinates) - if surface == SurfaceTypes.TOP: - z_extremum = np.max(coordinates[:, 2]) - compare = lambda z: z >= z_extremum - depth - neighbor_check = lambda z, neighbors: not any(coordinates[n][2] > z for n in neighbors) - elif surface == SurfaceTypes.BOTTOM: - z_extremum = np.min(coordinates[:, 2]) - compare = lambda z: z <= z_extremum + depth - neighbor_check = lambda z, neighbors: not any(coordinates[n][2] < z for n in neighbors) - else: - raise ValueError(f"Surface must be {SurfaceTypes.TOP} or {SurfaceTypes.BOTTOM}") + z_extremum = np.max(coordinates[:, 2]) if surface == SurfaceTypes.TOP else np.min(coordinates[:, 2]) + depth_check = lambda z: (z >= z_extremum - depth) if surface == SurfaceTypes.TOP else (z <= z_extremum + depth) + shadow_check = lambda z, neighbors: not any( + (coordinates[n][2] > z if surface == SurfaceTypes.TOP else coordinates[n][2] < z) for n in neighbors + ) exposed_atoms_indices = [] for idx, (x, y, z) in enumerate(coordinates): - if compare(z): - neighbors = kd_tree.query_ball_point([x, y, z + shadowing_radius], r=shadowing_radius) - if neighbor_check(z, neighbors): + if depth_check(z): + neighbors = kd_tree.query_ball_point([x, y, z], r=shadowing_radius) + if shadow_check(z, neighbors): exposed_atoms_indices.append(ids[idx]) return exposed_atoms_indices From 0235224c26b361a72f51cc799490a1f6f5c1e650 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 13:52:38 -0700 Subject: [PATCH 48/89] chore: renames --- .../made/tools/build/passivation/__init__.py | 183 ------------------ .../tools/build/passivation/configuration.py | 6 +- .../made/tools/build/passivation/enums.py | 4 +- tests/py/unit/fixtures.py | 2 +- 4 files changed, 6 insertions(+), 189 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/passivation/__init__.py b/src/py/mat3ra/made/tools/build/passivation/__init__.py index 85277154..e69de29b 100644 --- a/src/py/mat3ra/made/tools/build/passivation/__init__.py +++ b/src/py/mat3ra/made/tools/build/passivation/__init__.py @@ -1,183 +0,0 @@ -def get_passivant_coordinate_crystal( - material: Material, atom_coordinate: np.ndarray, bond_direction: np.ndarray, bond_length: float = 2.0 -): - bond_normal_cartesian = np.array(material.basis.cell.convert_point_to_cartesian(bond_direction.tolist())) - bond_normal_cartesian /= np.linalg.norm(bond_normal_cartesian) - bond_vector_cartesian = bond_normal_cartesian * bond_length - bond_vector_crystal = np.array(material.basis.cell.convert_point_to_crystal(bond_vector_cartesian.tolist())) - passivant_coordinate_crystal = atom_coordinate + bond_vector_crystal - return passivant_coordinate_crystal - - -def passivate_material( - slab: Material, passivant: str, bond_length: float = 2.0, coordination_threshold: Optional[int] = None -): - nudge_value = 0.01 - supercell_scaling_factor = [3, 3, 3] - min_coordinate = [1 / 3, 1 / 3, 1 / 3] - max_coordinate = [2 / 3, 2 / 3, 2 / 3] - adjusted_min_coordinate = (np.array(min_coordinate) - nudge_value).tolist() - adjusted_max_coordinate = (np.array(max_coordinate) + nudge_value).tolist() - slab = translate_to_z_level(slab, "center") - new_basis = slab.basis.copy() - slab_supercell = create_supercell(slab, scaling_factor=supercell_scaling_factor) - - central_cell_ids = [ - id - for id, coordinate in zip(slab_supercell.basis.coordinates.ids, slab_supercell.basis.coordinates.values) - if is_coordinate_in_box(coordinate, adjusted_min_coordinate, adjusted_max_coordinate) - ] - undercoordinated_atom_indices, atom_neighbors_info = get_undercoordinated_atoms( - slab_supercell, central_cell_ids, coordination_threshold - ) - for index in undercoordinated_atom_indices: - atom_coordinate_crystal = np.array(slab_supercell.basis.coordinates.values[index]) - neighbors_average_coordinate_crystal = atom_neighbors_info[index][2] - bond_normal_crystal = np.array(slab_supercell.basis.coordinates.values[index]) - np.array( - neighbors_average_coordinate_crystal - ) - passivant_coordinate_crystal = get_passivant_coordinate_crystal( - slab_supercell, atom_coordinate_crystal, bond_normal_crystal, bond_length - ).tolist() - passivant_coordinate_crystal_original_cell = transform_coordinate_to_supercell( - passivant_coordinate_crystal, - scaling_factor=supercell_scaling_factor, - translation_vector=min_coordinate, - reverse=True, - ) - new_basis.add_atom(passivant, passivant_coordinate_crystal_original_cell) - - slab.basis = new_basis - return slab - - -def add_passivant_atoms_to_material( - material: Material, - axis: Literal["x", "y", "z"], - surface: Optional[SURFACE_TYPES] = SURFACE_TYPES.BOTH, - tolerance: float = 0.01, - passivant: Optional[str] = "H", - default_bond_length: float = 1.0, -) -> Material: - """ - Add passivant atoms to the specified surface or edge of the material. - - Args: - material (Material): The material object to add passivant atoms to. - axis (AXIS_TYPES): The axis along which the surface or edge lies ("x", "y", or "z"). - surface (SURFACE_TYPES): The surface to add passivant atoms to ("top", "bottom", or "both"). - tolerance (float): The tolerance for selecting surface atoms. - passivant (str): The chemical symbol of the passivating atom (e.g., 'H'). - default_bond_length (float): The default bond length to use if the pair is not found in BOND_LENGTHS_MAP. - - Returns: - Material: The material object with passivation atoms added. - """ - axis_idx = {"x": 0, "y": 1, "z": 2}[axis] - basis = material.basis.copy() - if axis == "z": - if surface in [SURFACE_TYPES.TOP, SURFACE_TYPES.BOTH]: - z_extremum = get_atomic_coordinates_extremum(material, "max", axis) - add_passivant_atoms_to_basis( - material, basis, axis_idx, z_extremum, tolerance, passivant, default_bond_length, positive=True - ) - if surface in [SURFACE_TYPES.BOTTOM, SURFACE_TYPES.BOTH]: - z_extremum = get_atomic_coordinates_extremum(material, "min", axis) - add_passivant_atoms_to_basis( - material, basis, axis_idx, z_extremum, tolerance, passivant, default_bond_length, positive=False - ) - else: - x_or_y_extremum_max = get_atomic_coordinates_extremum(material, "max", axis) - add_passivant_atoms_to_basis( - material, basis, axis_idx, x_or_y_extremum_max, tolerance, passivant, default_bond_length, positive=True - ) - - x_or_y_extremum_min = get_atomic_coordinates_extremum(material, "min", axis) - add_passivant_atoms_to_basis( - material, basis, axis_idx, x_or_y_extremum_min, tolerance, passivant, default_bond_length, positive=False - ) - material.basis = basis - return material - - -def add_passivant_atoms_to_basis( - material, basis, axis_idx, extremum, tolerance, passivant, default_bond_length, positive=True -): - """ - Helper function to add passivant atoms to the specified extremum along a given axis. - """ - min_box = [0, 0, 0] - max_box = [1, 1, 1] - min_box[axis_idx] = extremum - tolerance - max_box[axis_idx] = extremum + tolerance - - surface_atoms = filter_by_box(material, min_box, max_box) - surface_indices = surface_atoms.basis.coordinates.ids - - for idx in surface_indices: - atom_coordinate = material.basis.coordinates.values[idx] - element = material.basis.elements.values[idx] - bond_length = BOND_LENGTHS_MAP.get((element, passivant), default_bond_length) - - bond_length_vector = [0, 0, 0] - bond_length_vector[axis_idx] = bond_length if positive else -bond_length - bond_length_crystal = material.basis.cell.convert_point_to_crystal(bond_length_vector) - - passivant_coordinate = atom_coordinate + bond_length_crystal - basis.add_atom(passivant, passivant_coordinate) - - -def passivate_surface( - material: Material, - passivant: str = "H", - default_bond_length: float = 1.0, - surface: SURFACE_TYPES = SURFACE_TYPES.BOTH, -) -> Material: - """ - Passivates the top and/or bottom surfaces of a material by adding atoms along the Z-axis, - with bond lengths determined by the element and passivant. - - Args: - material (Material): The material to passivate. - passivant (str): The chemical symbol of the passivating atom (e.g., 'H'). - default_bond_length (float): The default bond length to use if the pair is not found in BOND_LENGTHS_MAP. - surface (SURFACE_TYPES): The surface to passivate ("top", "bottom", or "both"). - - Returns: - Material: The passivated material. - """ - material = translate_to_z_level(material, "center") - - return add_passivant_atoms_to_material( - material=material, - axis="z", - surface=surface, - passivant=passivant, - default_bond_length=default_bond_length, - ) - - -def passivate_edges( - material: Material, - passivant: str = "H", - default_bond_length: float = 1.0, - axis: Literal["x", "y"] = "y", -) -> Material: - """ - Passivates the edges of a 2D material by adding atoms along the X or Y axis, - with bond lengths determined by the element and passivant. - - Args: - material (Material): The material to passivate. - passivant (str): The chemical symbol of the passivating atom (e.g., 'H'). - default_bond_length (float): The default bond length to use if the pair is not found in BOND_LENGTHS_MAP. - axis (AXIS_TYPES): The axis along which the edges lie ("x" or "y"). - - Returns: - Material: The passivated material. - """ - material = translate_to_z_level(material, "center") - - return add_passivant_atoms_to_material( - material=material, axis=axis, passivant=passivant, default_bond_length=default_bond_length - ) diff --git a/src/py/mat3ra/made/tools/build/passivation/configuration.py b/src/py/mat3ra/made/tools/build/passivation/configuration.py index b8a5837d..815447d9 100644 --- a/src/py/mat3ra/made/tools/build/passivation/configuration.py +++ b/src/py/mat3ra/made/tools/build/passivation/configuration.py @@ -1,6 +1,6 @@ -from typing import Optional - from mat3ra.made.material import Material + +from .enums import SurfaceTypes, EdgeTypes from ...build import BaseConfiguration @@ -10,7 +10,7 @@ class PassivationConfiguration(BaseConfiguration): Args: slab (Material): The Material object. - passivant (str): The passivant element. + passivant (str): The passivating element. bond_length (float): The bond length. """ diff --git a/src/py/mat3ra/made/tools/build/passivation/enums.py b/src/py/mat3ra/made/tools/build/passivation/enums.py index 53a85c23..74321e86 100644 --- a/src/py/mat3ra/made/tools/build/passivation/enums.py +++ b/src/py/mat3ra/made/tools/build/passivation/enums.py @@ -15,6 +15,6 @@ class SurfaceTypes(str, Enum): class EdgeTypes(str, Enum): - ALONG_A = "along_a" - ALONG_B = "along_b" + ALONG_X = "along_x" + ALONG_Y = "along_y" BOTH = "both" diff --git a/tests/py/unit/fixtures.py b/tests/py/unit/fixtures.py index 112bd810..088f4cd4 100644 --- a/tests/py/unit/fixtures.py +++ b/tests/py/unit/fixtures.py @@ -228,7 +228,7 @@ SI_SLAB_PASSIVATED = { - "name": "Si8(001), termination Si_P4/mmm_1, Slab", + "name": "Si8(001), termination Si_P4/mmm_1, Slab H-passivated", "basis": { "elements": [ {"id": 0, "value": "Si"}, From 203c0c9eaab66837a941b14e3b67c93c123d25a4 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 13:53:10 -0700 Subject: [PATCH 49/89] update: remove from modify --- src/py/mat3ra/made/tools/modify.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index eef5ad52..f43bc242 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -6,8 +6,6 @@ get_atom_indices_with_condition_on_coordinates, get_atom_indices_within_radius_pbc, get_atomic_coordinates_extremum, - get_undercoordinated_atoms, - get_surface_atoms_indices, ) from .convert import from_ase, to_ase from .third_party import ase_add_vacuum @@ -464,31 +462,3 @@ def rotate_material(material: Material, axis: List[int], angle: float) -> Materi atoms.wrap() return Material(from_ase(atoms)) - - -def passivate_top(material: Material, passivant: str = "H", bond_length=1.0, shadowing_radius: float = 2.5, depth=10): - """ - Passivate the top surface of the material with the specified element. - - Args: - material (Material): Material object to passivate. - passivant (str): Element to use for passivation. - bond_length (float): Bond length to use for the passivation in Angstroms. - shadowing_radius (float): Radius for atoms shadowing underlying from passivation in Angstroms. - depth (float): Depth from the top surface to passivate in Angstroms. - - Returns: - Material: Material object with the top surface passivated. - """ - new_material = material.clone() - top_atom_indices = get_surface_atoms_indices(material, shadowing_radius=shadowing_radius, depth=depth) - top_atoms = filter_material_by_ids(material, top_atom_indices) - new_material.to_cartesian() - top_atoms.to_cartesian() - top_atoms_coordinates = top_atoms.basis.coordinates.values - passivant_coordinates = np.array(top_atoms_coordinates) + [0, 0, bond_length] - - for coordinate in passivant_coordinates: - new_material.add_atom(passivant, coordinate) - new_material.to_crystal() - return new_material From 77d64bcfcd94e99114c25d778a73c709e5ef7231 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:00:36 -0700 Subject: [PATCH 50/89] update: adjust test to use new builders --- tests/py/unit/test_tools_build_passivation.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/py/unit/test_tools_build_passivation.py b/tests/py/unit/test_tools_build_passivation.py index fd856882..1f0291c4 100644 --- a/tests/py/unit/test_tools_build_passivation.py +++ b/tests/py/unit/test_tools_build_passivation.py @@ -1,15 +1,16 @@ from mat3ra.made.material import Material - -from mat3ra.made.tools.modify import ( - passivate_surface, -) +from mat3ra.made.tools.build.passivation.builders import SurfacePassivationBuilder, SurfacePassivationBuilderParameters +from mat3ra.made.tools.build.passivation.configuration import SurfacePassivationConfiguration from mat3ra.utils import assertion as assertion_utils from .fixtures import SI_SLAB, SI_SLAB_PASSIVATED def test_passivate_surface(): - passivated_material = passivate_surface( - material=Material(SI_SLAB), passivant="H", default_bond_length=1.48, surface="both" + config = SurfacePassivationConfiguration(slab=Material(SI_SLAB), passivant="H", bond_length=1.48, surface="both") + builder = SurfacePassivationBuilder( + build_parameters=SurfacePassivationBuilderParameters(shadowing_radius=2.5, depth=2.0) ) + passivated_material = builder.get_material(config) + print(passivated_material.to_json()) assertion_utils.assert_deep_almost_equal(SI_SLAB_PASSIVATED, passivated_material.to_json()) From 23f172d362f88ef38dac4552769885f6498426cd Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:03:51 -0700 Subject: [PATCH 51/89] update: add metadata to fixture --- tests/py/unit/fixtures.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/py/unit/fixtures.py b/tests/py/unit/fixtures.py index 088f4cd4..a9a2a61c 100644 --- a/tests/py/unit/fixtures.py +++ b/tests/py/unit/fixtures.py @@ -243,8 +243,8 @@ {"id": 1, "value": [0.5, 0.0, 0.437499993]}, {"id": 2, "value": [0.0, 0.0, 0.562499993]}, {"id": 3, "value": [0.0, 0.5, 0.687499993]}, - {"id": 4, "value": [0.0, 0.5, 0.822813932]}, - {"id": 5, "value": [0.5, 0.5, 0.177186054]}, + {"id": 4, "value": [0.5, 0.5, 0.177186054]}, + {"id": 5, "value": [0.0, 0.5, 0.822813932]}, ], "units": "crystal", "cell": [[3.867, 0.0, 0.0], [-0.0, 3.867, 0.0], [0.0, 0.0, 10.937528]], @@ -269,7 +269,18 @@ }, "isNonPeriodic": False, "_id": "", - "metadata": SI_SLAB["metadata"], + "metadata": { + "boundaryConditions": {"type": "pbc", "offset": 0}, + "build": { + "configuration": { + "type": "SurfacePassivationConfiguration", + "slab": SI_SLAB, + "passivant": "H", + "bond_length": 1.48, + }, + "termination": "Si_P4/mmm_1", + }, + }, "isUpdated": True, } From e29882919aa45d29193ce3c5c63628b8eab64947 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:06:56 -0700 Subject: [PATCH 52/89] chore: run lint fix --- src/py/mat3ra/made/tools/analyze.py | 16 ++++++++++------ tests/py/unit/test_tools_modify.py | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index 51823e8c..d22e893e 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -339,6 +339,14 @@ def get_atomic_coordinates_extremum( return getattr(np, extremum)(values) +def depth_check(z, z_extremum, depth, surface): + return (z >= z_extremum - depth) if surface == SurfaceTypes.TOP else (z <= z_extremum + depth) + + +def shadow_check(z, neighbors, surface, coordinates): + return not any((coordinates[n][2] > z if surface == SurfaceTypes.TOP else coordinates[n][2] < z) for n in neighbors) + + def get_surface_atoms_indices( material: Material, surface: SurfaceTypes = SurfaceTypes.TOP, shadowing_radius: float = 2.5, depth: float = 5 ) -> List[int]: @@ -361,16 +369,12 @@ def get_surface_atoms_indices( kd_tree = cKDTree(coordinates) z_extremum = np.max(coordinates[:, 2]) if surface == SurfaceTypes.TOP else np.min(coordinates[:, 2]) - depth_check = lambda z: (z >= z_extremum - depth) if surface == SurfaceTypes.TOP else (z <= z_extremum + depth) - shadow_check = lambda z, neighbors: not any( - (coordinates[n][2] > z if surface == SurfaceTypes.TOP else coordinates[n][2] < z) for n in neighbors - ) exposed_atoms_indices = [] for idx, (x, y, z) in enumerate(coordinates): - if depth_check(z): + if depth_check(z, z_extremum, depth, surface): neighbors = kd_tree.query_ball_point([x, y, z], r=shadowing_radius) - if shadow_check(z, neighbors): + if shadow_check(z, neighbors, surface, coordinates): exposed_atoms_indices.append(ids[idx]) return exposed_atoms_indices diff --git a/tests/py/unit/test_tools_modify.py b/tests/py/unit/test_tools_modify.py index 8960f023..9c848c2e 100644 --- a/tests/py/unit/test_tools_modify.py +++ b/tests/py/unit/test_tools_modify.py @@ -15,7 +15,7 @@ ) from mat3ra.utils import assertion as assertion_utils -from .fixtures import SI_CONVENTIONAL_CELL, SI_SLAB, SI_SLAB_PASSIVATED, SI_SLAB_VACUUM +from .fixtures import SI_CONVENTIONAL_CELL, SI_SLAB, SI_SLAB_VACUUM COMMON_PART = { "units": "crystal", From 3ddb0e772763cad0f11cad7de6bf6c3058fd4bc2 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:22:03 -0700 Subject: [PATCH 53/89] update: apply OOP --- .../made/tools/build/passivation/builders.py | 55 +++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/passivation/builders.py b/src/py/mat3ra/made/tools/build/passivation/builders.py index 4741fe24..23c31a1f 100644 --- a/src/py/mat3ra/made/tools/build/passivation/builders.py +++ b/src/py/mat3ra/made/tools/build/passivation/builders.py @@ -72,45 +72,44 @@ class SurfacePassivationBuilder(PassivationBuilder): def create_passivated_material(self, configuration: SurfacePassivationConfiguration) -> Material: material = super().create_passivated_material(configuration) - passivant_coordinates_values_top = np.array([]) - passivant_coordinates_values_bottom = np.array([]) - - if configuration.surface == SurfaceTypes.TOP or configuration.surface == SurfaceTypes.BOTH: - passivant_coordinates_values_top = self._get_passivant_coordinates( - material, - SurfaceTypes.TOP, - configuration.bond_length, - self.build_parameters.shadowing_radius, - self.build_parameters.depth, - ) + passivant_coordinates_values = [] - if configuration.surface == SurfaceTypes.BOTTOM or configuration.surface == SurfaceTypes.BOTH: - passivant_coordinates_values_bottom = self._get_passivant_coordinates( - material, - SurfaceTypes.BOTTOM, - configuration.bond_length, - self.build_parameters.shadowing_radius, - self.build_parameters.depth, + if configuration.surface in (SurfaceTypes.BOTTOM, SurfaceTypes.BOTH): + passivant_coordinates_values.extend( + self._get_passivant_coordinates(material, SurfaceTypes.BOTTOM, configuration) + ) + if configuration.surface in (SurfaceTypes.TOP, SurfaceTypes.BOTH): + passivant_coordinates_values.extend( + self._get_passivant_coordinates(material, SurfaceTypes.TOP, configuration) ) - passivant_coordinates_values = ( - passivant_coordinates_values_bottom.tolist() + passivant_coordinates_values_top.tolist() - ) return self._add_passivant_atoms(material, passivant_coordinates_values, configuration.passivant) - def _get_passivant_coordinates(self, material, surface, bond_length, shadowing_radius, depth): + def _get_passivant_coordinates( + self, material: Material, surface: SurfaceTypes, configuration: SurfacePassivationConfiguration + ): + """ + Calculate the coordinates for placing passivants based on the specified surface type. + + Args: + material (Material): Material to passivate. + surface (SurfaceTypes): Surface type (TOP or BOTTOM). + configuration (SurfacePassivationConfiguration): Configuration for passivation. + + Returns: + list: Coordinates where passivants should be added. + """ surface_atoms_indices = get_surface_atoms_indices( - material=material, - surface=surface, - shadowing_radius=shadowing_radius, - depth=depth, + material, surface, self.build_parameters.shadowing_radius, self.build_parameters.depth ) surface_atoms_coordinates = [ material.basis.coordinates.get_element_value_by_index(i) for i in surface_atoms_indices ] - bond_vector = [0, 0, bond_length] if surface == SurfaceTypes.TOP else [0, 0, -bond_length] + bond_vector = ( + [0, 0, configuration.bond_length] if surface == SurfaceTypes.TOP else [0, 0, -configuration.bond_length] + ) passivant_bond_vector_crystal = material.basis.cell.convert_point_to_crystal(bond_vector) - return np.array(surface_atoms_coordinates) + np.array(passivant_bond_vector_crystal) + return (np.array(surface_atoms_coordinates) + np.array(passivant_bond_vector_crystal)).tolist() class EdgePassivationBuilder(PassivationBuilder): From 3af57011fa55a9233d88a66fbc5b8121b28480e5 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:23:41 -0700 Subject: [PATCH 54/89] feat: add undercocodintion detection --- src/py/mat3ra/made/tools/analyze.py | 34 ++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index d22e893e..6d22acdc 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -347,7 +347,7 @@ def shadow_check(z, neighbors, surface, coordinates): return not any((coordinates[n][2] > z if surface == SurfaceTypes.TOP else coordinates[n][2] < z) for n in neighbors) -def get_surface_atoms_indices( +def get_surface_atom_indices( material: Material, surface: SurfaceTypes = SurfaceTypes.TOP, shadowing_radius: float = 2.5, depth: float = 5 ) -> List[int]: """ @@ -378,3 +378,35 @@ def get_surface_atoms_indices( exposed_atoms_indices.append(ids[idx]) return exposed_atoms_indices + + +def get_undercoordinated_atom_indices( + material: Material, surface: SurfaceTypes = SurfaceTypes.TOP, coordination_number: int = 3, cutoff: float = 3.0 +) -> List[int]: + """ + Identify undercoordinated atoms on the top or bottom surface of the material. + + Args: + material (Material): Material object to get undercoordinated atoms from. + surface (SurfaceTypes): Specify "top" or "bottom" to detect the respective surface atoms. + coordination_number (int): The coordination number to detect undercoordinated atoms. + + Returns: + List[int]: List of indices of undercoordinated surface atoms. + """ + new_material = material.clone() + new_material.to_cartesian() + coordinates = np.array(new_material.basis.coordinates.values) + ids = new_material.basis.coordinates.ids + kd_tree = cKDTree(coordinates) + + z_extremum = np.max(coordinates[:, 2]) if surface == SurfaceTypes.TOP else np.min(coordinates[:, 2]) + + undercoordinated_atoms_indices = [] + for idx, (x, y, z) in enumerate(coordinates): + if z == z_extremum: + neighbors = kd_tree.query_ball_point([x, y, z], r=cutoff) + if len(neighbors) < coordination_number: + undercoordinated_atoms_indices.append(ids[idx]) + + return undercoordinated_atoms_indices From 7b5323e962bab9b65844a0d4c4fcfe00a73ce52c Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:24:03 -0700 Subject: [PATCH 55/89] update: use undercoordination --- .../made/tools/build/passivation/builders.py | 58 ++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/passivation/builders.py b/src/py/mat3ra/made/tools/build/passivation/builders.py index 23c31a1f..499cc2e6 100644 --- a/src/py/mat3ra/made/tools/build/passivation/builders.py +++ b/src/py/mat3ra/made/tools/build/passivation/builders.py @@ -5,10 +5,14 @@ from pydantic import BaseModel from .enums import SurfaceTypes -from ...analyze import get_surface_atoms_indices +from ...analyze import get_surface_atom_indices, get_undercoordinated_atom_indices from ...modify import translate_to_z_level from ...build import BaseBuilder -from .configuration import PassivationConfiguration, SurfacePassivationConfiguration, EdgePassivationConfiguration +from .configuration import ( + PassivationConfiguration, + SurfacePassivationConfiguration, + UndercoordinationPassivationConfiguration, +) class PassivationBuilder(BaseBuilder): @@ -70,6 +74,9 @@ class SurfacePassivationBuilder(PassivationBuilder): Detects surface atoms looking along Z axis and passivates either the top or bottom surface or both. """ + build_parameters: SurfacePassivationBuilderParameters = SurfacePassivationBuilderParameters() + _ConfigurationType = SurfacePassivationConfiguration + def create_passivated_material(self, configuration: SurfacePassivationConfiguration) -> Material: material = super().create_passivated_material(configuration) passivant_coordinates_values = [] @@ -99,7 +106,7 @@ def _get_passivant_coordinates( Returns: list: Coordinates where passivants should be added. """ - surface_atoms_indices = get_surface_atoms_indices( + surface_atoms_indices = get_surface_atom_indices( material, surface, self.build_parameters.shadowing_radius, self.build_parameters.depth ) surface_atoms_coordinates = [ @@ -112,12 +119,47 @@ def _get_passivant_coordinates( return (np.array(surface_atoms_coordinates) + np.array(passivant_bond_vector_crystal)).tolist() -class EdgePassivationBuilder(PassivationBuilder): +class UndercoordinationPassivationBuilderParameters(BaseModel): + """ + Parameters for the UndercoordinationPassivationBuilder. + Args: + coordination_threshold (int): The coordination threshold for undercoordination. + """ + + cutoff: float = 3.0 + coordination_threshold: int = 3 + + +class UndercoordinationPassivationBuilder(PassivationBuilder): """ - Builder for passivating an edge. + Builder for passivating material based on undercoordination of atoms. - Detects edge atoms looking perpendicular to the Z axis and passivates them. + Detects atoms with coordination number below a threshold and passivates them. """ - def create_passivated_material(self, configuration: EdgePassivationConfiguration) -> Material: - pass + build_parameters: UndercoordinationPassivationBuilderParameters = UndercoordinationPassivationBuilderParameters() + + def create_passivated_material(self, configuration: UndercoordinationPassivationConfiguration) -> Material: + material = super().create_passivated_material(configuration) + passivant_coordinates_values = self._get_passivant_coordinates(material, configuration) + return self._add_passivant_atoms(material, passivant_coordinates_values, configuration.passivant) + + def _get_passivant_coordinates(self, material: Material, configuration: UndercoordinationPassivationConfiguration): + """ + Calculate the coordinates for placing passivants based on the specified edge type. + + Args: + material (Material): Material to passivate. + configuration (EdgePassivationConfiguration): Configuration for passivation. + """ + edge_atoms_indices = get_undercoordinated_atom_indices( + material=material, + surface=SurfaceTypes.TOP, + coordination_number=self.build_parameters.coordination_threshold, + cutoff=self.build_parameters.cutoff, + ) + edge_atoms_coordinates = [material.basis.coordinates.get_element_value_by_index(i) for i in edge_atoms_indices] + bond_vector = [0, 0, configuration.bond_length] + + passivant_bond_vector_crystal = material.basis.cell.convert_point_to_crystal(bond_vector) + return (np.array(edge_atoms_coordinates) + passivant_bond_vector_crystal).tolist() From ff5f611bc3d0cfd3f40b0ac502ab5e87ae45a493 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:25:26 -0700 Subject: [PATCH 56/89] update: remove redundant --- .../made/tools/build/passivation/builders.py | 5 ++--- .../tools/build/passivation/configuration.py | 16 ++++++++-------- .../mat3ra/made/tools/build/passivation/enums.py | 6 ------ 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/passivation/builders.py b/src/py/mat3ra/made/tools/build/passivation/builders.py index 499cc2e6..470e1ab4 100644 --- a/src/py/mat3ra/made/tools/build/passivation/builders.py +++ b/src/py/mat3ra/made/tools/build/passivation/builders.py @@ -11,7 +11,6 @@ from .configuration import ( PassivationConfiguration, SurfacePassivationConfiguration, - UndercoordinationPassivationConfiguration, ) @@ -139,12 +138,12 @@ class UndercoordinationPassivationBuilder(PassivationBuilder): build_parameters: UndercoordinationPassivationBuilderParameters = UndercoordinationPassivationBuilderParameters() - def create_passivated_material(self, configuration: UndercoordinationPassivationConfiguration) -> Material: + def create_passivated_material(self, configuration: SurfacePassivationConfiguration) -> Material: material = super().create_passivated_material(configuration) passivant_coordinates_values = self._get_passivant_coordinates(material, configuration) return self._add_passivant_atoms(material, passivant_coordinates_values, configuration.passivant) - def _get_passivant_coordinates(self, material: Material, configuration: UndercoordinationPassivationConfiguration): + def _get_passivant_coordinates(self, material: Material, configuration: SurfacePassivationConfiguration): """ Calculate the coordinates for placing passivants based on the specified edge type. diff --git a/src/py/mat3ra/made/tools/build/passivation/configuration.py b/src/py/mat3ra/made/tools/build/passivation/configuration.py index 815447d9..81db55f6 100644 --- a/src/py/mat3ra/made/tools/build/passivation/configuration.py +++ b/src/py/mat3ra/made/tools/build/passivation/configuration.py @@ -1,6 +1,6 @@ from mat3ra.made.material import Material -from .enums import SurfaceTypes, EdgeTypes +from .enums import SurfaceTypes from ...build import BaseConfiguration @@ -35,10 +35,10 @@ class SurfacePassivationConfiguration(PassivationConfiguration): surface: SurfaceTypes = SurfaceTypes.BOTH - -class EdgePassivationConfiguration(PassivationConfiguration): - """ - Configuration for a passivation. - """ - - edge: EdgeTypes = EdgeTypes.BOTH + @property + def _json(self): + return { + **super()._json, + "type": self.get_cls_name(), + "surface": self.surface, + } diff --git a/src/py/mat3ra/made/tools/build/passivation/enums.py b/src/py/mat3ra/made/tools/build/passivation/enums.py index 74321e86..ffec5ffb 100644 --- a/src/py/mat3ra/made/tools/build/passivation/enums.py +++ b/src/py/mat3ra/made/tools/build/passivation/enums.py @@ -12,9 +12,3 @@ class SurfaceTypes(str, Enum): TOP = "top" BOTTOM = "bottom" BOTH = "both" - - -class EdgeTypes(str, Enum): - ALONG_X = "along_x" - ALONG_Y = "along_y" - BOTH = "both" From 0634b9dcab53e0abd2bd950e234b1915ffa30290 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:42:15 -0700 Subject: [PATCH 57/89] update: add vector away from local neighbors --- .../made/tools/build/passivation/builders.py | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/passivation/builders.py b/src/py/mat3ra/made/tools/build/passivation/builders.py index 470e1ab4..263bc620 100644 --- a/src/py/mat3ra/made/tools/build/passivation/builders.py +++ b/src/py/mat3ra/made/tools/build/passivation/builders.py @@ -5,7 +5,7 @@ from pydantic import BaseModel from .enums import SurfaceTypes -from ...analyze import get_surface_atom_indices, get_undercoordinated_atom_indices +from ...analyze import get_surface_atom_indices, get_undercoordinated_atom_indices, get_nearest_neighbors_atom_indices from ...modify import translate_to_z_level from ...build import BaseBuilder from .configuration import ( @@ -149,16 +149,36 @@ def _get_passivant_coordinates(self, material: Material, configuration: SurfaceP Args: material (Material): Material to passivate. - configuration (EdgePassivationConfiguration): Configuration for passivation. + configuration (SurfacePassivationConfiguration): Configuration for passivation. """ - edge_atoms_indices = get_undercoordinated_atom_indices( + undercoordinated_atoms_indices = get_undercoordinated_atom_indices( material=material, surface=SurfaceTypes.TOP, coordination_number=self.build_parameters.coordination_threshold, cutoff=self.build_parameters.cutoff, ) - edge_atoms_coordinates = [material.basis.coordinates.get_element_value_by_index(i) for i in edge_atoms_indices] - bond_vector = [0, 0, configuration.bond_length] - passivant_bond_vector_crystal = material.basis.cell.convert_point_to_crystal(bond_vector) - return (np.array(edge_atoms_coordinates) + passivant_bond_vector_crystal).tolist() + passivant_coordinates = [] + + for idx in undercoordinated_atoms_indices: + nearest_neighbors = get_nearest_neighbors_atom_indices( + material=material, + coordinate=material.basis.coordinates.get_element_value_by_index(idx), + cutoff=self.build_parameters.cutoff, + ) + + if nearest_neighbors is None: + continue + average_coordinate = np.mean( + [material.basis.coordinates.get_element_value_by_index(i) for i in nearest_neighbors], axis=0 + ) + + bond_vector = material.basis.coordinates.get_element_value_by_index(idx) - average_coordinate + bond_vector = bond_vector / np.linalg.norm(bond_vector) * configuration.bond_length + passivant_bond_vector_crystal = material.basis.cell.convert_point_to_crystal(bond_vector) + + passivant_coordinates.append( + material.basis.coordinates.get_element_value_by_index(idx) + passivant_bond_vector_crystal + ) + + return passivant_coordinates From de30779aa1159f859fc7c1c95ed58f280c21a979 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:45:41 -0700 Subject: [PATCH 58/89] update: simplify --- .../made/tools/build/passivation/builders.py | 11 +++++------ .../tools/build/passivation/configuration.py | 18 ++---------------- tests/py/unit/fixtures.py | 3 ++- tests/py/unit/test_tools_build_passivation.py | 5 ++--- 4 files changed, 11 insertions(+), 26 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/passivation/builders.py b/src/py/mat3ra/made/tools/build/passivation/builders.py index 263bc620..6be044ce 100644 --- a/src/py/mat3ra/made/tools/build/passivation/builders.py +++ b/src/py/mat3ra/made/tools/build/passivation/builders.py @@ -10,7 +10,6 @@ from ...build import BaseBuilder from .configuration import ( PassivationConfiguration, - SurfacePassivationConfiguration, ) @@ -74,9 +73,9 @@ class SurfacePassivationBuilder(PassivationBuilder): """ build_parameters: SurfacePassivationBuilderParameters = SurfacePassivationBuilderParameters() - _ConfigurationType = SurfacePassivationConfiguration + _ConfigurationType = PassivationConfiguration - def create_passivated_material(self, configuration: SurfacePassivationConfiguration) -> Material: + def create_passivated_material(self, configuration: PassivationConfiguration) -> Material: material = super().create_passivated_material(configuration) passivant_coordinates_values = [] @@ -92,7 +91,7 @@ def create_passivated_material(self, configuration: SurfacePassivationConfigurat return self._add_passivant_atoms(material, passivant_coordinates_values, configuration.passivant) def _get_passivant_coordinates( - self, material: Material, surface: SurfaceTypes, configuration: SurfacePassivationConfiguration + self, material: Material, surface: SurfaceTypes, configuration: PassivationConfiguration ): """ Calculate the coordinates for placing passivants based on the specified surface type. @@ -138,12 +137,12 @@ class UndercoordinationPassivationBuilder(PassivationBuilder): build_parameters: UndercoordinationPassivationBuilderParameters = UndercoordinationPassivationBuilderParameters() - def create_passivated_material(self, configuration: SurfacePassivationConfiguration) -> Material: + def create_passivated_material(self, configuration: PassivationConfiguration) -> Material: material = super().create_passivated_material(configuration) passivant_coordinates_values = self._get_passivant_coordinates(material, configuration) return self._add_passivant_atoms(material, passivant_coordinates_values, configuration.passivant) - def _get_passivant_coordinates(self, material: Material, configuration: SurfacePassivationConfiguration): + def _get_passivant_coordinates(self, material: Material, configuration: PassivationConfiguration): """ Calculate the coordinates for placing passivants based on the specified edge type. diff --git a/src/py/mat3ra/made/tools/build/passivation/configuration.py b/src/py/mat3ra/made/tools/build/passivation/configuration.py index 81db55f6..10a73b88 100644 --- a/src/py/mat3ra/made/tools/build/passivation/configuration.py +++ b/src/py/mat3ra/made/tools/build/passivation/configuration.py @@ -17,6 +17,7 @@ class PassivationConfiguration(BaseConfiguration): slab: Material passivant: str = "H" bond_length: float = 1.0 + surface: SurfaceTypes = SurfaceTypes.BOTH @property def _json(self): @@ -25,20 +26,5 @@ def _json(self): "slab": self.slab.to_json(), "passivant": self.passivant, "bond_length": self.bond_length, - } - - -class SurfacePassivationConfiguration(PassivationConfiguration): - """ - Configuration for a passivation. - """ - - surface: SurfaceTypes = SurfaceTypes.BOTH - - @property - def _json(self): - return { - **super()._json, - "type": self.get_cls_name(), - "surface": self.surface, + "surface": self.surface.value, } diff --git a/tests/py/unit/fixtures.py b/tests/py/unit/fixtures.py index a9a2a61c..d6cdc874 100644 --- a/tests/py/unit/fixtures.py +++ b/tests/py/unit/fixtures.py @@ -273,10 +273,11 @@ "boundaryConditions": {"type": "pbc", "offset": 0}, "build": { "configuration": { - "type": "SurfacePassivationConfiguration", + "type": "PassivationConfiguration", "slab": SI_SLAB, "passivant": "H", "bond_length": 1.48, + "surface": "both", }, "termination": "Si_P4/mmm_1", }, diff --git a/tests/py/unit/test_tools_build_passivation.py b/tests/py/unit/test_tools_build_passivation.py index 1f0291c4..469b73cc 100644 --- a/tests/py/unit/test_tools_build_passivation.py +++ b/tests/py/unit/test_tools_build_passivation.py @@ -1,16 +1,15 @@ from mat3ra.made.material import Material from mat3ra.made.tools.build.passivation.builders import SurfacePassivationBuilder, SurfacePassivationBuilderParameters -from mat3ra.made.tools.build.passivation.configuration import SurfacePassivationConfiguration +from mat3ra.made.tools.build.passivation.configuration import PassivationConfiguration from mat3ra.utils import assertion as assertion_utils from .fixtures import SI_SLAB, SI_SLAB_PASSIVATED def test_passivate_surface(): - config = SurfacePassivationConfiguration(slab=Material(SI_SLAB), passivant="H", bond_length=1.48, surface="both") + config = PassivationConfiguration(slab=Material(SI_SLAB), passivant="H", bond_length=1.48, surface="both") builder = SurfacePassivationBuilder( build_parameters=SurfacePassivationBuilderParameters(shadowing_radius=2.5, depth=2.0) ) passivated_material = builder.get_material(config) - print(passivated_material.to_json()) assertion_utils.assert_deep_almost_equal(SI_SLAB_PASSIVATED, passivated_material.to_json()) From 1623de19865d87950213d01bf425d3fb6dd360be Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 27 Aug 2024 10:43:24 -0700 Subject: [PATCH 59/89] chore: renames and add types --- src/py/mat3ra/made/tools/analyze.py | 15 +++++++++------ .../mat3ra/made/tools/build/passivation/enums.py | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index 6d22acdc..d7cf5f5b 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -339,12 +339,14 @@ def get_atomic_coordinates_extremum( return getattr(np, extremum)(values) -def depth_check(z, z_extremum, depth, surface): +def height_check(z: float, z_extremum: float, depth: float, surface: SurfaceTypes): return (z >= z_extremum - depth) if surface == SurfaceTypes.TOP else (z <= z_extremum + depth) -def shadow_check(z, neighbors, surface, coordinates): - return not any((coordinates[n][2] > z if surface == SurfaceTypes.TOP else coordinates[n][2] < z) for n in neighbors) +def shadowing_check(z: float, neighbors_indices: List[int], surface: SurfaceTypes, coordinates: np.ndarray): + return not any( + (coordinates[n][2] > z if surface == SurfaceTypes.TOP else coordinates[n][2] < z) for n in neighbors_indices + ) def get_surface_atom_indices( @@ -372,9 +374,10 @@ def get_surface_atom_indices( exposed_atoms_indices = [] for idx, (x, y, z) in enumerate(coordinates): - if depth_check(z, z_extremum, depth, surface): - neighbors = kd_tree.query_ball_point([x, y, z], r=shadowing_radius) - if shadow_check(z, neighbors, surface, coordinates): + if height_check(z, z_extremum, depth, surface): + neighbors_indices = kd_tree.query_ball_point([x, y, z], r=shadowing_radius) + print("neighbors_indices", type(neighbors_indices), neighbors_indices) + if shadowing_check(z, neighbors_indices, surface, coordinates): exposed_atoms_indices.append(ids[idx]) return exposed_atoms_indices diff --git a/src/py/mat3ra/made/tools/build/passivation/enums.py b/src/py/mat3ra/made/tools/build/passivation/enums.py index ffec5ffb..dba18bb4 100644 --- a/src/py/mat3ra/made/tools/build/passivation/enums.py +++ b/src/py/mat3ra/made/tools/build/passivation/enums.py @@ -1,4 +1,4 @@ -# TODO: Get this from peroidic table +# TODO: Get this from periodic table from enum import Enum BOND_LENGTHS_MAP = { From 6199ed4005a6036a50db5fbe8380bbf2a66b967c Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 27 Aug 2024 16:00:36 -0700 Subject: [PATCH 60/89] update: simplify and OOP --- src/py/mat3ra/made/tools/analyze.py | 55 +++++++++++----- .../made/tools/build/passivation/builders.py | 65 ++++++++++++++----- 2 files changed, 86 insertions(+), 34 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index d7cf5f5b..889b6a88 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -376,40 +376,63 @@ def get_surface_atom_indices( for idx, (x, y, z) in enumerate(coordinates): if height_check(z, z_extremum, depth, surface): neighbors_indices = kd_tree.query_ball_point([x, y, z], r=shadowing_radius) - print("neighbors_indices", type(neighbors_indices), neighbors_indices) if shadowing_check(z, neighbors_indices, surface, coordinates): exposed_atoms_indices.append(ids[idx]) return exposed_atoms_indices -def get_undercoordinated_atom_indices( - material: Material, surface: SurfaceTypes = SurfaceTypes.TOP, coordination_number: int = 3, cutoff: float = 3.0 +def get_coordination_numbers( + material: Material, + indices: Optional[List[int]] = None, + cutoff: float = 3.0, ) -> List[int]: """ - Identify undercoordinated atoms on the top or bottom surface of the material. + Calculate the coordination numbers of atoms in the material. Args: - material (Material): Material object to get undercoordinated atoms from. - surface (SurfaceTypes): Specify "top" or "bottom" to detect the respective surface atoms. - coordination_number (int): The coordination number to detect undercoordinated atoms. + material (Material): Material object to calculate coordination numbers for. + indices (List[int]): List of atom indices to calculate coordination numbers for. + cutoff (float): The cutoff radius for identifying neighbors. Returns: - List[int]: List of indices of undercoordinated surface atoms. + List[int]: List of coordination numbers for each atom in the material. """ new_material = material.clone() new_material.to_cartesian() + if indices is not None: + new_material.basis.coordinates.filter_by_indices(indices) coordinates = np.array(new_material.basis.coordinates.values) - ids = new_material.basis.coordinates.ids kd_tree = cKDTree(coordinates) - z_extremum = np.max(coordinates[:, 2]) if surface == SurfaceTypes.TOP else np.min(coordinates[:, 2]) - - undercoordinated_atoms_indices = [] + coordination_numbers = [] for idx, (x, y, z) in enumerate(coordinates): - if z == z_extremum: - neighbors = kd_tree.query_ball_point([x, y, z], r=cutoff) - if len(neighbors) < coordination_number: - undercoordinated_atoms_indices.append(ids[idx]) + neighbors = kd_tree.query_ball_point([x, y, z], r=cutoff) + # Explicitly remove the atom itself from the list of neighbors + neighbors = [n for n in neighbors if n != idx] + coordination_numbers.append(len(neighbors)) + + return coordination_numbers + +def get_undercoordinated_atom_indices( + material: Material, + indices: List[int], + cutoff: float = 3.0, + coordination_threshold: int = 3, +) -> List[int]: + """ + Identify undercoordinated atoms among the specified indices in the material. + + Args: + material (Material): Material object to identify undercoordinated atoms in. + indices (List[int]): List of atom indices to check for undercoordination. + cutoff (float): The cutoff radius for identifying neighbors. + coordination_threshold (int): The coordination number threshold for undercoordination. + + Returns: + List[int]: List of indices of undercoordinated atoms. + """ + coordination_numbers = get_coordination_numbers(material, indices, cutoff) + undercoordinated_atoms_indices = [i for i, cn in enumerate(coordination_numbers) if cn <= coordination_threshold] return undercoordinated_atoms_indices diff --git a/src/py/mat3ra/made/tools/build/passivation/builders.py b/src/py/mat3ra/made/tools/build/passivation/builders.py index 6be044ce..576efac8 100644 --- a/src/py/mat3ra/made/tools/build/passivation/builders.py +++ b/src/py/mat3ra/made/tools/build/passivation/builders.py @@ -5,7 +5,12 @@ from pydantic import BaseModel from .enums import SurfaceTypes -from ...analyze import get_surface_atom_indices, get_undercoordinated_atom_indices, get_nearest_neighbors_atom_indices +from ...analyze import ( + get_surface_atom_indices, + get_undercoordinated_atom_indices, + get_nearest_neighbors_atom_indices, + get_coordination_numbers, +) from ...modify import translate_to_z_level from ...build import BaseBuilder from .configuration import ( @@ -117,14 +122,13 @@ def _get_passivant_coordinates( return (np.array(surface_atoms_coordinates) + np.array(passivant_bond_vector_crystal)).tolist() -class UndercoordinationPassivationBuilderParameters(BaseModel): +class UndercoordinationPassivationBuilderParameters(SurfacePassivationBuilderParameters): """ Parameters for the UndercoordinationPassivationBuilder. Args: coordination_threshold (int): The coordination threshold for undercoordination. """ - cutoff: float = 3.0 coordination_threshold: int = 3 @@ -135,43 +139,51 @@ class UndercoordinationPassivationBuilder(PassivationBuilder): Detects atoms with coordination number below a threshold and passivates them. """ - build_parameters: UndercoordinationPassivationBuilderParameters = UndercoordinationPassivationBuilderParameters() + _BuildParametersType = UndercoordinationPassivationBuilderParameters + _DefaultBuildParameters = UndercoordinationPassivationBuilderParameters() def create_passivated_material(self, configuration: PassivationConfiguration) -> Material: material = super().create_passivated_material(configuration) - passivant_coordinates_values = self._get_passivant_coordinates(material, configuration) + surface_atoms_indices = get_surface_atom_indices( + material=material, + surface=SurfaceTypes.TOP, + shadowing_radius=self.build_parameters.shadowing_radius, + depth=self.build_parameters.depth, + ) + undercoordinated_atoms_indices = get_undercoordinated_atom_indices( + material=material, + indices=surface_atoms_indices, + cutoff=self.build_parameters.shadowing_radius, + coordination_threshold=self.build_parameters.coordination_threshold, + ) + passivant_coordinates_values = self._get_passivant_coordinates( + material, configuration, undercoordinated_atoms_indices + ) return self._add_passivant_atoms(material, passivant_coordinates_values, configuration.passivant) - def _get_passivant_coordinates(self, material: Material, configuration: PassivationConfiguration): + def _get_passivant_coordinates( + self, material: Material, configuration: PassivationConfiguration, undercoordinated_atoms_indices: list + ): """ - Calculate the coordinates for placing passivants based on the specified edge type. + Calculate the coordinates for placing passivating atoms based on the specified edge type. Args: material (Material): Material to passivate. configuration (SurfacePassivationConfiguration): Configuration for passivation. + undercoordinated_atoms_indices (list): Indices of undercoordinated atoms. """ - undercoordinated_atoms_indices = get_undercoordinated_atom_indices( - material=material, - surface=SurfaceTypes.TOP, - coordination_number=self.build_parameters.coordination_threshold, - cutoff=self.build_parameters.cutoff, - ) - passivant_coordinates = [] - for idx in undercoordinated_atoms_indices: nearest_neighbors = get_nearest_neighbors_atom_indices( material=material, coordinate=material.basis.coordinates.get_element_value_by_index(idx), - cutoff=self.build_parameters.cutoff, + cutoff=self.build_parameters.shadowing_radius, ) - if nearest_neighbors is None: continue average_coordinate = np.mean( [material.basis.coordinates.get_element_value_by_index(i) for i in nearest_neighbors], axis=0 ) - bond_vector = material.basis.coordinates.get_element_value_by_index(idx) - average_coordinate bond_vector = bond_vector / np.linalg.norm(bond_vector) * configuration.bond_length passivant_bond_vector_crystal = material.basis.cell.convert_point_to_crystal(bond_vector) @@ -181,3 +193,20 @@ def _get_passivant_coordinates(self, material: Material, configuration: Passivat ) return passivant_coordinates + + def get_coordination_numbers(self, material: Material): + """ + Get the coordination numbers for all atoms in the material. + + Args: + material (Material): The material object. + + Returns: + set: The coordination numbers for all atoms in the material. + """ + + coordination_numbers = set( + get_coordination_numbers(material=material, cutoff=self.build_parameters.shadowing_radius) + ) + print("coordination numbers:", coordination_numbers) + return coordination_numbers From c4125b1447123d554d85d2fa26c0c0f414d846ab Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 28 Aug 2024 19:28:21 -0700 Subject: [PATCH 61/89] feat: add PBC handler decorator --- src/py/mat3ra/made/tools/analyze.py | 2 + src/py/mat3ra/made/tools/utils/__init__.py | 52 ++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index 889b6a88..7b5ec8a4 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -1,6 +1,7 @@ from typing import Callable, List, Literal, Optional import numpy as np +from .utils import decorator_handle_periodic_boundary_conditions from scipy.spatial import cKDTree from ..material import Material @@ -415,6 +416,7 @@ def get_coordination_numbers( return coordination_numbers +@decorator_handle_periodic_boundary_conditions(cutoff=3.0) def get_undercoordinated_atom_indices( material: Material, indices: List[int], diff --git a/src/py/mat3ra/made/tools/utils/__init__.py b/src/py/mat3ra/made/tools/utils/__init__.py index 95c961eb..ece960b1 100644 --- a/src/py/mat3ra/made/tools/utils/__init__.py +++ b/src/py/mat3ra/made/tools/utils/__init__.py @@ -106,3 +106,55 @@ def transform_coordinate_to_supercell( if reverse: converted_array = (np_coordinate - np_translation_vector) * np_scaling_factor return converted_array.tolist() + + +def decorator_handle_periodic_boundary_conditions(cutoff): + def decorator(func): + @wraps(func) + def wrapper(material, *args, **kwargs): + # Augment material with periodic images + augmented_material, original_count = augment_material_with_periodic_images(material, cutoff) + + # Call the original function with augmented material + result = func(augmented_material, *args, **kwargs) + + # Filter results to include only original atoms + if isinstance(result, list): # Assuming result is a list of indices + result = [idx for idx in result if idx < original_count] + return result + + return wrapper + + return decorator + + +def augment_material_with_periodic_images(material, cutoff): + """Augment the material's dataset by adding atoms from periodic images near boundaries.""" + from ..build.utils import merge_materials + from ..modify import filter_by_box, translate_by_vector + + material = material.clone() + original_count = len(material.basis.coordinates.values) + material_slice_x1 = filter_by_box(material, [0, 0, 0], [cutoff, 1, 1]) + material_slice_x2 = filter_by_box(material, [1 - cutoff, 0, 0], [1, 1, 1]) + translated_material_slice_x1 = translate_by_vector(material_slice_x1, [1, 0, 0]) + translated_material_slice_x2 = translate_by_vector(material_slice_x2, [-1, 0, 0]) + augmented_material_x = merge_materials([material, translated_material_slice_x1, translated_material_slice_x2]) + + material_slice_y1 = filter_by_box(augmented_material_x, [-cutoff, 0, 0], [1 + cutoff, cutoff, 1]) + material_slice_y2 = filter_by_box(augmented_material_x, [-cutoff, 1 - cutoff, 0], [1 + cutoff, 1, 1]) + translated_material_slice_y1 = translate_by_vector(material_slice_y1, [0, 1, 0]) + translated_material_slice_y2 = translate_by_vector(material_slice_y2, [0, -1, 0]) + augmented_material_y = merge_materials( + [augmented_material_x, translated_material_slice_y1, translated_material_slice_y2] + ) + + material_slice_z1 = filter_by_box(augmented_material_y, [-cutoff, -cutoff, 0], [1 + cutoff, 1 + cutoff, cutoff]) + material_slice_z2 = filter_by_box(augmented_material_y, [-cutoff, -cutoff, 1 - cutoff], [1 + cutoff, 1 + cutoff, 1]) + translated_material_slice_z1 = translate_by_vector(material_slice_z1, [0, 0, 1]) + translated_material_slice_z2 = translate_by_vector(material_slice_z2, [0, 0, -1]) + augmented_material = merge_materials( + [augmented_material_y, translated_material_slice_z1, translated_material_slice_z2] + ) + + return augmented_material, original_count From 8e1e69263e1823583c95b1187618f706323b814b Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 28 Aug 2024 19:56:23 -0700 Subject: [PATCH 62/89] udpate: optimize performance and deps --- src/py/mat3ra/made/tools/analyze.py | 3 +- src/py/mat3ra/made/tools/utils/__init__.py | 83 ++++++++++++++-------- 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index 7b5ec8a4..d3ae305f 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -350,6 +350,7 @@ def shadowing_check(z: float, neighbors_indices: List[int], surface: SurfaceType ) +@decorator_handle_periodic_boundary_conditions(cutoff=3.0) def get_surface_atom_indices( material: Material, surface: SurfaceTypes = SurfaceTypes.TOP, shadowing_radius: float = 2.5, depth: float = 5 ) -> List[int]: @@ -416,7 +417,7 @@ def get_coordination_numbers( return coordination_numbers -@decorator_handle_periodic_boundary_conditions(cutoff=3.0) +@decorator_handle_periodic_boundary_conditions(cutoff=0.1) def get_undercoordinated_atom_indices( material: Material, indices: List[int], diff --git a/src/py/mat3ra/made/tools/utils/__init__.py b/src/py/mat3ra/made/tools/utils/__init__.py index ece960b1..c3fd325d 100644 --- a/src/py/mat3ra/made/tools/utils/__init__.py +++ b/src/py/mat3ra/made/tools/utils/__init__.py @@ -2,6 +2,7 @@ from typing import Callable, List, Optional import numpy as np +from mat3ra.made.material import Material from mat3ra.utils.matrix import convert_2x2_to_3x3 from ..third_party import PymatgenStructure @@ -119,7 +120,7 @@ def wrapper(material, *args, **kwargs): result = func(augmented_material, *args, **kwargs) # Filter results to include only original atoms - if isinstance(result, list): # Assuming result is a list of indices + if isinstance(result, list): result = [idx for idx in result if idx < original_count] return result @@ -128,33 +129,59 @@ def wrapper(material, *args, **kwargs): return decorator -def augment_material_with_periodic_images(material, cutoff): - """Augment the material's dataset by adding atoms from periodic images near boundaries.""" - from ..build.utils import merge_materials - from ..modify import filter_by_box, translate_by_vector +def filter_and_translate(coordinates: np.ndarray, elements: np.ndarray, axis: int, cutoff: float, direction: int): + """ + Filter and translate atom coordinates based on the axis and direction. + + Args: + coordinates (np.ndarray): The coordinates of the atoms. + elements (np.ndarray): The elements of the atoms. + axis (int): The axis to filter and translate. + cutoff (float): The cutoff value for filtering. + direction (int): The direction to translate. + + Returns: + Tuple[np.ndarray, np.ndarray]: The filtered and translated coordinates and elements. + """ + mask = (coordinates[:, axis] < cutoff) if direction == 1 else (coordinates[:, axis] > (1 - cutoff)) + filtered_coordinates = coordinates[mask] + filtered_elements = elements[mask] + translation_vector = np.zeros(3) + translation_vector[axis] = direction + translated_coordinates = filtered_coordinates + translation_vector + return translated_coordinates, filtered_elements - material = material.clone() - original_count = len(material.basis.coordinates.values) - material_slice_x1 = filter_by_box(material, [0, 0, 0], [cutoff, 1, 1]) - material_slice_x2 = filter_by_box(material, [1 - cutoff, 0, 0], [1, 1, 1]) - translated_material_slice_x1 = translate_by_vector(material_slice_x1, [1, 0, 0]) - translated_material_slice_x2 = translate_by_vector(material_slice_x2, [-1, 0, 0]) - augmented_material_x = merge_materials([material, translated_material_slice_x1, translated_material_slice_x2]) - - material_slice_y1 = filter_by_box(augmented_material_x, [-cutoff, 0, 0], [1 + cutoff, cutoff, 1]) - material_slice_y2 = filter_by_box(augmented_material_x, [-cutoff, 1 - cutoff, 0], [1 + cutoff, 1, 1]) - translated_material_slice_y1 = translate_by_vector(material_slice_y1, [0, 1, 0]) - translated_material_slice_y2 = translate_by_vector(material_slice_y2, [0, -1, 0]) - augmented_material_y = merge_materials( - [augmented_material_x, translated_material_slice_y1, translated_material_slice_y2] - ) - - material_slice_z1 = filter_by_box(augmented_material_y, [-cutoff, -cutoff, 0], [1 + cutoff, 1 + cutoff, cutoff]) - material_slice_z2 = filter_by_box(augmented_material_y, [-cutoff, -cutoff, 1 - cutoff], [1 + cutoff, 1 + cutoff, 1]) - translated_material_slice_z1 = translate_by_vector(material_slice_z1, [0, 0, 1]) - translated_material_slice_z2 = translate_by_vector(material_slice_z2, [0, 0, -1]) - augmented_material = merge_materials( - [augmented_material_y, translated_material_slice_z1, translated_material_slice_z2] - ) +def augment_material_with_periodic_images(material: Material, cutoff: float = 0.1): + """ + Augment the material's dataset by adding atoms from periodic images near boundaries. + + Args: + material (Material): The material to augment. + cutoff (float): The cutoff value for filtering atoms near boundaries. + + Returns: + Tuple[Material, int]: The augmented material and the original count of atoms. + """ + original_count = len(material.basis.coordinates.values) + coordinates = np.array(material.basis.coordinates.values) + elements = np.array(material.basis.elements.values) + augmented_coords = [] + augmented_elems = [] + + for axis in range(3): + for direction in [-1, 1]: + translated_coords, translated_elems = filter_and_translate(coordinates, elements, axis, cutoff, direction) + augmented_coords.append(translated_coords) + augmented_elems.append(translated_elems) + + augmented_coords = np.vstack(augmented_coords).tolist() + augmented_elems = np.concatenate(augmented_elems).tolist() + augmented_material = material.clone() + + new_basis = material.basis.copy() + for i, coord in enumerate(augmented_coords): + new_basis.add_atom(augmented_elems[i], coord) + + augmented_material.basis = new_basis return augmented_material, original_count From e908fe6e5284ba5cefbc6cea96cfbae4f2b567d8 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 28 Aug 2024 20:24:38 -0700 Subject: [PATCH 63/89] update: some fixes for tests to pass --- src/py/mat3ra/made/tools/analyze.py | 3 ++- src/py/mat3ra/made/tools/utils/__init__.py | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index d3ae305f..afb3c41c 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -350,7 +350,7 @@ def shadowing_check(z: float, neighbors_indices: List[int], surface: SurfaceType ) -@decorator_handle_periodic_boundary_conditions(cutoff=3.0) +@decorator_handle_periodic_boundary_conditions(cutoff=0.1) def get_surface_atom_indices( material: Material, surface: SurfaceTypes = SurfaceTypes.TOP, shadowing_radius: float = 2.5, depth: float = 5 ) -> List[int]: @@ -417,6 +417,7 @@ def get_coordination_numbers( return coordination_numbers +# TODO: fix decorator removal of added atoms and uncomment @decorator_handle_periodic_boundary_conditions(cutoff=0.1) def get_undercoordinated_atom_indices( material: Material, diff --git a/src/py/mat3ra/made/tools/utils/__init__.py b/src/py/mat3ra/made/tools/utils/__init__.py index c3fd325d..392ee17b 100644 --- a/src/py/mat3ra/made/tools/utils/__init__.py +++ b/src/py/mat3ra/made/tools/utils/__init__.py @@ -113,15 +113,15 @@ def decorator_handle_periodic_boundary_conditions(cutoff): def decorator(func): @wraps(func) def wrapper(material, *args, **kwargs): - # Augment material with periodic images augmented_material, original_count = augment_material_with_periodic_images(material, cutoff) - - # Call the original function with augmented material result = func(augmented_material, *args, **kwargs) - # Filter results to include only original atoms if isinstance(result, list): result = [idx for idx in result if idx < original_count] + + if isinstance(result, list) and all(isinstance(coord, float) for coord in result): + result = [coordinate for coordinate in result if 0 <= coordinate <= 1] + return result return wrapper @@ -179,7 +179,7 @@ def augment_material_with_periodic_images(material: Material, cutoff: float = 0. augmented_elems = np.concatenate(augmented_elems).tolist() augmented_material = material.clone() - new_basis = material.basis.copy() + new_basis = augmented_material.basis.copy() for i, coord in enumerate(augmented_coords): new_basis.add_atom(augmented_elems[i], coord) From e7df9f42e7341fa266f3a5fd9883f9f18abb7f13 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:52:43 -0700 Subject: [PATCH 64/89] update: correctly handle diagonal cells --- src/py/mat3ra/made/tools/analyze.py | 1 - src/py/mat3ra/made/tools/utils/__init__.py | 13 ++++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index afb3c41c..15f49ad6 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -417,7 +417,6 @@ def get_coordination_numbers( return coordination_numbers -# TODO: fix decorator removal of added atoms and uncomment @decorator_handle_periodic_boundary_conditions(cutoff=0.1) def get_undercoordinated_atom_indices( material: Material, diff --git a/src/py/mat3ra/made/tools/utils/__init__.py b/src/py/mat3ra/made/tools/utils/__init__.py index 392ee17b..4c3dbd0b 100644 --- a/src/py/mat3ra/made/tools/utils/__init__.py +++ b/src/py/mat3ra/made/tools/utils/__init__.py @@ -166,19 +166,18 @@ def augment_material_with_periodic_images(material: Material, cutoff: float = 0. original_count = len(material.basis.coordinates.values) coordinates = np.array(material.basis.coordinates.values) elements = np.array(material.basis.elements.values) - augmented_coords = [] - augmented_elems = [] + augmented_coords = coordinates.tolist() + augmented_elems = elements.tolist() for axis in range(3): for direction in [-1, 1]: translated_coords, translated_elems = filter_and_translate(coordinates, elements, axis, cutoff, direction) - augmented_coords.append(translated_coords) - augmented_elems.append(translated_elems) + augmented_coords.extend(translated_coords) + augmented_elems.extend(translated_elems) + coordinates = np.array(augmented_coords) + elements = np.array(augmented_elems) - augmented_coords = np.vstack(augmented_coords).tolist() - augmented_elems = np.concatenate(augmented_elems).tolist() augmented_material = material.clone() - new_basis = augmented_material.basis.copy() for i, coord in enumerate(augmented_coords): new_basis.add_atom(augmented_elems[i], coord) From cbc413163857657122006110501c44f1aafe7284 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 29 Aug 2024 13:30:08 -0700 Subject: [PATCH 65/89] update: fix exessive after decorator --- src/py/mat3ra/made/tools/utils/__init__.py | 47 +++++++++++++--------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/src/py/mat3ra/made/tools/utils/__init__.py b/src/py/mat3ra/made/tools/utils/__init__.py index 4c3dbd0b..1ac833cc 100644 --- a/src/py/mat3ra/made/tools/utils/__init__.py +++ b/src/py/mat3ra/made/tools/utils/__init__.py @@ -3,6 +3,7 @@ import numpy as np from mat3ra.made.material import Material +from mat3ra.made.utils import ArrayWithIds from mat3ra.utils.matrix import convert_2x2_to_3x3 from ..third_party import PymatgenStructure @@ -110,18 +111,32 @@ def transform_coordinate_to_supercell( def decorator_handle_periodic_boundary_conditions(cutoff): + """ + Decorator to handle periodic boundary conditions. + + Copies atoms near boundaries within the cutoff distance beyond the opposite side of the cell + creating the effect of periodic boundary conditions for edge atoms. + + Results of the function are filtered to remove atoms or coordinates outside the original cell. + + Args: + cutoff (float): The cutoff distance for a border slice in crystal coordinates. + + Returns: + Callable: The decorated function. + """ + def decorator(func): @wraps(func) def wrapper(material, *args, **kwargs): - augmented_material, original_count = augment_material_with_periodic_images(material, cutoff) + augmented_material, last_id = augment_material_with_periodic_images(material, cutoff) result = func(augmented_material, *args, **kwargs) if isinstance(result, list): - result = [idx for idx in result if idx < original_count] - - if isinstance(result, list) and all(isinstance(coord, float) for coord in result): - result = [coordinate for coordinate in result if 0 <= coordinate <= 1] - + if all(isinstance(x, int) for x in result): + result = [id for id in result if id <= last_id] + elif all(isinstance(x, list) and len(x) == 3 for x in result): + result = [coord for coord in result if all(0 <= c < 1 for c in coord)] return result return wrapper @@ -163,24 +178,18 @@ def augment_material_with_periodic_images(material: Material, cutoff: float = 0. Returns: Tuple[Material, int]: The augmented material and the original count of atoms. """ - original_count = len(material.basis.coordinates.values) + last_id = material.basis.coordinates.ids[-1] coordinates = np.array(material.basis.coordinates.values) elements = np.array(material.basis.elements.values) - augmented_coords = coordinates.tolist() - augmented_elems = elements.tolist() + augmented_material = material.clone() + new_basis = augmented_material.basis.copy() for axis in range(3): for direction in [-1, 1]: translated_coords, translated_elems = filter_and_translate(coordinates, elements, axis, cutoff, direction) - augmented_coords.extend(translated_coords) - augmented_elems.extend(translated_elems) - coordinates = np.array(augmented_coords) - elements = np.array(augmented_elems) - - augmented_material = material.clone() - new_basis = augmented_material.basis.copy() - for i, coord in enumerate(augmented_coords): - new_basis.add_atom(augmented_elems[i], coord) + for coord, elem in zip(translated_coords, translated_elems): + if not any(np.allclose(coord, existing_coord) for existing_coord in new_basis.coordinates.values): + new_basis.add_atom(elem, coord) augmented_material.basis = new_basis - return augmented_material, original_count + return augmented_material, last_id From 499d8ae779d832bef2d0c9f4b4f281751ac74a8a Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 29 Aug 2024 20:09:36 -0700 Subject: [PATCH 66/89] update: add try for faulty nn --- src/py/mat3ra/made/tools/analyze.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index 15f49ad6..a209f07c 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -300,8 +300,10 @@ def get_nearest_neighbors_atom_indices( site_index = len(structure.sites) - 1 remove_dummy_atom = True - - neighbors = voronoi_nn.get_nn_info(structure, site_index) + try: + neighbors = voronoi_nn.get_nn_info(structure, site_index) + except: + return None neighboring_atoms_pymatgen_ids = [n["site_index"] for n in neighbors] if remove_dummy_atom: structure.remove_sites([-1]) From bd996d5cb8eedaf191cd49506f3b37e4263c02ab Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 29 Aug 2024 20:09:55 -0700 Subject: [PATCH 67/89] update: don't add colliding atom --- src/py/mat3ra/made/basis.py | 12 ++++++++++-- src/py/mat3ra/made/tools/utils/__init__.py | 3 +-- src/py/mat3ra/made/utils.py | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/py/mat3ra/made/basis.py b/src/py/mat3ra/made/basis.py index da34c13c..21c043ec 100644 --- a/src/py/mat3ra/made/basis.py +++ b/src/py/mat3ra/made/basis.py @@ -6,7 +6,7 @@ from pydantic import BaseModel from .cell import Cell -from .utils import ArrayWithIds +from .utils import ArrayWithIds, get_overlapping_coordinates class Basis(RoundNumericValuesMixin, BaseModel): @@ -76,7 +76,15 @@ def to_crystal(self): self.coordinates.map_array_in_place(self.cell.convert_point_to_crystal) self.units = AtomicCoordinateUnits.crystal - def add_atom(self, element="Si", coordinate=[0.5, 0.5, 0.5]): + def add_atom(self, element="Si", coordinate=None, force=False): + if coordinate is None: + coordinate = [0, 0, 0] + if get_overlapping_coordinates(coordinate, self.coordinates.values, threshold=0.01): + if force: + print(f"Warning: Overlapping coordinates found for {coordinate}. Adding atom anyway.") + else: + print(f"Warning: Overlapping coordinates found for {coordinate}. Not adding atom.") + return self.elements.add_item(element) self.coordinates.add_item(coordinate) diff --git a/src/py/mat3ra/made/tools/utils/__init__.py b/src/py/mat3ra/made/tools/utils/__init__.py index 1ac833cc..964dbfcf 100644 --- a/src/py/mat3ra/made/tools/utils/__init__.py +++ b/src/py/mat3ra/made/tools/utils/__init__.py @@ -188,8 +188,7 @@ def augment_material_with_periodic_images(material: Material, cutoff: float = 0. for direction in [-1, 1]: translated_coords, translated_elems = filter_and_translate(coordinates, elements, axis, cutoff, direction) for coord, elem in zip(translated_coords, translated_elems): - if not any(np.allclose(coord, existing_coord) for existing_coord in new_basis.coordinates.values): - new_basis.add_atom(elem, coord) + new_basis.add_atom(elem, coord) augmented_material.basis = new_basis return augmented_material, last_id diff --git a/src/py/mat3ra/made/utils.py b/src/py/mat3ra/made/utils.py index 38ca10c3..cd8a7e33 100644 --- a/src/py/mat3ra/made/utils.py +++ b/src/py/mat3ra/made/utils.py @@ -58,6 +58,23 @@ def get_center_of_coordinates(coordinates: List[List[float]]) -> List[float]: return np.mean(np.array(coordinates), axis=0).tolist() +def get_overlapping_coordinates( + coordinate: List[float], coordinates: List[List[float]], threshold: float = 0.01 +) -> List[List[float]]: + """ + Find coordinates that are within a certain threshold of a given coordinate. + + Args: + coordinate (List[float]): The coordinate. + coordinates (List[List[float]]): The list of coordinates. + threshold (float): The threshold. + + Returns: + List[List[float]]: The list of overlapping coordinates. + """ + return [c for c in coordinates if np.linalg.norm(np.array(c) - np.array(coordinate)) < threshold] + + class ValueWithId(RoundNumericValuesMixin, BaseModel): id: int = 0 value: Any = None From 008264db4f44d0a1dc2ae00d64652710f5fa8968 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 29 Aug 2024 20:18:22 -0700 Subject: [PATCH 68/89] chore: run lint fix --- src/py/mat3ra/made/tools/analyze.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index a209f07c..1912a88a 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -1,13 +1,13 @@ from typing import Callable, List, Literal, Optional import numpy as np -from .utils import decorator_handle_periodic_boundary_conditions from scipy.spatial import cKDTree from ..material import Material from .build.passivation.enums import SurfaceTypes from .convert import decorator_convert_material_args_kwargs_to_atoms, to_pymatgen from .third_party import ASEAtoms, PymatgenIStructure, PymatgenVoronoiNN +from .utils import decorator_handle_periodic_boundary_conditions @decorator_convert_material_args_kwargs_to_atoms @@ -302,7 +302,7 @@ def get_nearest_neighbors_atom_indices( remove_dummy_atom = True try: neighbors = voronoi_nn.get_nn_info(structure, site_index) - except: + except ValueError: return None neighboring_atoms_pymatgen_ids = [n["site_index"] for n in neighbors] if remove_dummy_atom: From f841cb6e01b9115cae622e838836eed303cb5243 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 8 Sep 2024 15:11:26 -0700 Subject: [PATCH 69/89] chore: add docstring --- src/py/mat3ra/made/tools/analyze.py | 34 +++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index 1912a88a..ef78bd26 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -277,7 +277,9 @@ def get_nearest_neighbors_atom_indices( Args: material (Material): The material object to find neighbors in. coordinate (List[float]): The position to find neighbors for. - cutoff (float): The cutoff radius for identifying neighbors. + tolerance (float): tolerance parameter for near-neighbor finding. Faces that are smaller than tol fraction + of the largest face are not included in the tessellation. (default: 0.1). + cutoff (float): The cutoff radius for identifying neighbors, in angstroms. Returns: List[int]: A list of indices of neighboring atoms, or an empty list if no neighbors are found. @@ -342,11 +344,35 @@ def get_atomic_coordinates_extremum( return getattr(np, extremum)(values) -def height_check(z: float, z_extremum: float, depth: float, surface: SurfaceTypes): +def is_height_within_limits(z: float, z_extremum: float, depth: float, surface: SurfaceTypes) -> bool: + """ + Check if the height of an atom is within the specified limits. + + Args: + z (float): The z-coordinate of the atom. + z_extremum (float): The extremum z-coordinate of the surface. + depth (float): The depth from the surface to look for exposed atoms. + surface (SurfaceTypes): The surface type (top or bottom). + + Returns: + bool: True if the height is within the limits, False otherwise. + """ return (z >= z_extremum - depth) if surface == SurfaceTypes.TOP else (z <= z_extremum + depth) -def shadowing_check(z: float, neighbors_indices: List[int], surface: SurfaceTypes, coordinates: np.ndarray): +def shadowing_check(z: float, neighbors_indices: List[int], surface: SurfaceTypes, coordinates: np.ndarray) -> bool: + """ + Check if the atom is shadowed by its neighbors from the surface. + + Args: + z (float): The z-coordinate of the atom. + neighbors_indices (List[int]): List of indices of neighboring atoms. + surface (SurfaceTypes): The surface type (top or bottom). + coordinates (np.ndarray): The coordinates of the atoms. + + Returns: + bool: True if the atom is not shadowed, False otherwise. + """ return not any( (coordinates[n][2] > z if surface == SurfaceTypes.TOP else coordinates[n][2] < z) for n in neighbors_indices ) @@ -378,7 +404,7 @@ def get_surface_atom_indices( exposed_atoms_indices = [] for idx, (x, y, z) in enumerate(coordinates): - if height_check(z, z_extremum, depth, surface): + if is_height_within_limits(z, z_extremum, depth, surface): neighbors_indices = kd_tree.query_ball_point([x, y, z], r=shadowing_radius) if shadowing_check(z, neighbors_indices, surface, coordinates): exposed_atoms_indices.append(ids[idx]) From 1cc19713656deed19b089a3d3b72ab82cbce54e1 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 8 Sep 2024 19:32:54 -0700 Subject: [PATCH 70/89] update: simplify and add docstring --- src/py/mat3ra/made/tools/utils/__init__.py | 37 ++++++---------------- src/py/mat3ra/made/utils.py | 6 ++-- 2 files changed, 14 insertions(+), 29 deletions(-) diff --git a/src/py/mat3ra/made/tools/utils/__init__.py b/src/py/mat3ra/made/tools/utils/__init__.py index 964dbfcf..6a2b6a16 100644 --- a/src/py/mat3ra/made/tools/utils/__init__.py +++ b/src/py/mat3ra/made/tools/utils/__init__.py @@ -144,36 +144,14 @@ def wrapper(material, *args, **kwargs): return decorator -def filter_and_translate(coordinates: np.ndarray, elements: np.ndarray, axis: int, cutoff: float, direction: int): - """ - Filter and translate atom coordinates based on the axis and direction. - - Args: - coordinates (np.ndarray): The coordinates of the atoms. - elements (np.ndarray): The elements of the atoms. - axis (int): The axis to filter and translate. - cutoff (float): The cutoff value for filtering. - direction (int): The direction to translate. - - Returns: - Tuple[np.ndarray, np.ndarray]: The filtered and translated coordinates and elements. - """ - mask = (coordinates[:, axis] < cutoff) if direction == 1 else (coordinates[:, axis] > (1 - cutoff)) - filtered_coordinates = coordinates[mask] - filtered_elements = elements[mask] - translation_vector = np.zeros(3) - translation_vector[axis] = direction - translated_coordinates = filtered_coordinates + translation_vector - return translated_coordinates, filtered_elements - - def augment_material_with_periodic_images(material: Material, cutoff: float = 0.1): """ - Augment the material's dataset by adding atoms from periodic images near boundaries. + Augment the material's dataset by adding atoms from periodic images within a cutoff distance from the boundaries by + copying them to the opposite side of the cell, translated by the cell vector beyond the boundary. Args: material (Material): The material to augment. - cutoff (float): The cutoff value for filtering atoms near boundaries. + cutoff (float): The cutoff value for filtering atoms near boundaries, in crystal coordinates. Returns: Tuple[Material, int]: The augmented material and the original count of atoms. @@ -186,8 +164,13 @@ def augment_material_with_periodic_images(material: Material, cutoff: float = 0. for axis in range(3): for direction in [-1, 1]: - translated_coords, translated_elems = filter_and_translate(coordinates, elements, axis, cutoff, direction) - for coord, elem in zip(translated_coords, translated_elems): + mask = (coordinates[:, axis] < cutoff) if direction == 1 else (coordinates[:, axis] > (1 - cutoff)) + filtered_coordinates = coordinates[mask] + filtered_elements = elements[mask] + translation_vector = np.zeros(3) + translation_vector[axis] = direction + translated_coordinates = filtered_coordinates + translation_vector + for coord, elem in zip(translated_coordinates, filtered_elements): new_basis.add_atom(elem, coord) augmented_material.basis = new_basis diff --git a/src/py/mat3ra/made/utils.py b/src/py/mat3ra/made/utils.py index cd8a7e33..d0acf855 100644 --- a/src/py/mat3ra/made/utils.py +++ b/src/py/mat3ra/made/utils.py @@ -59,7 +59,9 @@ def get_center_of_coordinates(coordinates: List[List[float]]) -> List[float]: def get_overlapping_coordinates( - coordinate: List[float], coordinates: List[List[float]], threshold: float = 0.01 + coordinate: List[float], + coordinates: List[List[float]], + threshold: float = 0.01, ) -> List[List[float]]: """ Find coordinates that are within a certain threshold of a given coordinate. @@ -67,7 +69,7 @@ def get_overlapping_coordinates( Args: coordinate (List[float]): The coordinate. coordinates (List[List[float]]): The list of coordinates. - threshold (float): The threshold. + threshold (float): The threshold for the distance, in the units of the coordinates. Returns: List[List[float]]: The list of overlapping coordinates. From 32461ef751dc88bfdf0114fd9c3ca08a9a2028b4 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 8 Sep 2024 19:33:19 -0700 Subject: [PATCH 71/89] update: add coreect cartesian detection of collision --- src/py/mat3ra/made/basis.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/py/mat3ra/made/basis.py b/src/py/mat3ra/made/basis.py index 21c043ec..2a42414c 100644 --- a/src/py/mat3ra/made/basis.py +++ b/src/py/mat3ra/made/basis.py @@ -79,7 +79,10 @@ def to_crystal(self): def add_atom(self, element="Si", coordinate=None, force=False): if coordinate is None: coordinate = [0, 0, 0] - if get_overlapping_coordinates(coordinate, self.coordinates.values, threshold=0.01): + cartesian_basis = self.copy() + cartesian_basis.to_cartesian() + cartesian_coordinate = self.cell.convert_point_to_cartesian(coordinate) + if get_overlapping_coordinates(cartesian_coordinate, cartesian_basis.coordinates.values, threshold=0.1): if force: print(f"Warning: Overlapping coordinates found for {coordinate}. Adding atom anyway.") else: From 4b04911686b249f6a17a9598d7a821a4421747d9 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:09:09 -0700 Subject: [PATCH 72/89] update: fix test --- src/py/mat3ra/made/basis.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/py/mat3ra/made/basis.py b/src/py/mat3ra/made/basis.py index 2a42414c..44f4dd4f 100644 --- a/src/py/mat3ra/made/basis.py +++ b/src/py/mat3ra/made/basis.py @@ -79,10 +79,9 @@ def to_crystal(self): def add_atom(self, element="Si", coordinate=None, force=False): if coordinate is None: coordinate = [0, 0, 0] - cartesian_basis = self.copy() - cartesian_basis.to_cartesian() + cartesian_coordinates = [self.cell.convert_point_to_cartesian(coord) for coord in self.coordinates.values] cartesian_coordinate = self.cell.convert_point_to_cartesian(coordinate) - if get_overlapping_coordinates(cartesian_coordinate, cartesian_basis.coordinates.values, threshold=0.1): + if get_overlapping_coordinates(cartesian_coordinate, cartesian_coordinates, threshold=0.01): if force: print(f"Warning: Overlapping coordinates found for {coordinate}. Adding atom anyway.") else: From 13d85382215972d3f82a08c685982f4e13a90638 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:35:36 -0700 Subject: [PATCH 73/89] update: add top level function to create passivation --- .../mat3ra/made/tools/build/passivation/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/py/mat3ra/made/tools/build/passivation/__init__.py b/src/py/mat3ra/made/tools/build/passivation/__init__.py index e69de29b..17be2ab5 100644 --- a/src/py/mat3ra/made/tools/build/passivation/__init__.py +++ b/src/py/mat3ra/made/tools/build/passivation/__init__.py @@ -0,0 +1,12 @@ +from typing import Union + +from mat3ra.made.material import Material +from .configuration import PassivationConfiguration +from .builders import PassivationBuilder, SurfacePassivationBuilder, UndercoordinationPassivationBuilder + + +def create_passivation( + configuration: PassivationConfiguration, + builder: Union[SurfacePassivationBuilder, UndercoordinationPassivationBuilder] = SurfacePassivationBuilder(), +) -> Material: + return builder.get_material(configuration) From 60e8b692ec73ec45455f06087653d87a46da2db0 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:04:24 -0700 Subject: [PATCH 74/89] update: move surface enums to top level to avoid circular deps --- src/py/mat3ra/made/tools/analyze.py | 2 +- src/py/mat3ra/made/tools/build/passivation/__init__.py | 2 +- src/py/mat3ra/made/tools/build/passivation/builders.py | 6 +++--- .../mat3ra/made/tools/build/passivation/configuration.py | 2 +- src/py/mat3ra/made/tools/build/passivation/enums.py | 7 ------- src/py/mat3ra/made/tools/enums.py | 7 +++++++ 6 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 src/py/mat3ra/made/tools/enums.py diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index ef78bd26..6b9ff0d0 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -4,7 +4,7 @@ from scipy.spatial import cKDTree from ..material import Material -from .build.passivation.enums import SurfaceTypes +from .enums import SurfaceTypes from .convert import decorator_convert_material_args_kwargs_to_atoms, to_pymatgen from .third_party import ASEAtoms, PymatgenIStructure, PymatgenVoronoiNN from .utils import decorator_handle_periodic_boundary_conditions diff --git a/src/py/mat3ra/made/tools/build/passivation/__init__.py b/src/py/mat3ra/made/tools/build/passivation/__init__.py index 17be2ab5..b19e061d 100644 --- a/src/py/mat3ra/made/tools/build/passivation/__init__.py +++ b/src/py/mat3ra/made/tools/build/passivation/__init__.py @@ -2,7 +2,7 @@ from mat3ra.made.material import Material from .configuration import PassivationConfiguration -from .builders import PassivationBuilder, SurfacePassivationBuilder, UndercoordinationPassivationBuilder +from .builders import SurfacePassivationBuilder, UndercoordinationPassivationBuilder def create_passivation( diff --git a/src/py/mat3ra/made/tools/build/passivation/builders.py b/src/py/mat3ra/made/tools/build/passivation/builders.py index 576efac8..134f611f 100644 --- a/src/py/mat3ra/made/tools/build/passivation/builders.py +++ b/src/py/mat3ra/made/tools/build/passivation/builders.py @@ -1,10 +1,10 @@ -from typing import List +from typing import List, Optional import numpy as np from mat3ra.made.material import Material from pydantic import BaseModel -from .enums import SurfaceTypes +from ...enums import SurfaceTypes from ...analyze import ( get_surface_atom_indices, get_undercoordinated_atom_indices, @@ -77,7 +77,7 @@ class SurfacePassivationBuilder(PassivationBuilder): Detects surface atoms looking along Z axis and passivates either the top or bottom surface or both. """ - build_parameters: SurfacePassivationBuilderParameters = SurfacePassivationBuilderParameters() + build_parameters: Optional[SurfacePassivationBuilderParameters] = SurfacePassivationBuilderParameters() _ConfigurationType = PassivationConfiguration def create_passivated_material(self, configuration: PassivationConfiguration) -> Material: diff --git a/src/py/mat3ra/made/tools/build/passivation/configuration.py b/src/py/mat3ra/made/tools/build/passivation/configuration.py index 10a73b88..a106f3ec 100644 --- a/src/py/mat3ra/made/tools/build/passivation/configuration.py +++ b/src/py/mat3ra/made/tools/build/passivation/configuration.py @@ -1,6 +1,6 @@ from mat3ra.made.material import Material -from .enums import SurfaceTypes +from ...enums import SurfaceTypes from ...build import BaseConfiguration diff --git a/src/py/mat3ra/made/tools/build/passivation/enums.py b/src/py/mat3ra/made/tools/build/passivation/enums.py index dba18bb4..feea29ef 100644 --- a/src/py/mat3ra/made/tools/build/passivation/enums.py +++ b/src/py/mat3ra/made/tools/build/passivation/enums.py @@ -1,14 +1,7 @@ # TODO: Get this from periodic table -from enum import Enum BOND_LENGTHS_MAP = { ("C", "H"): 1.09, ("Ni", "H"): 1.09, ("Si", "H"): 1.48, } - - -class SurfaceTypes(str, Enum): - TOP = "top" - BOTTOM = "bottom" - BOTH = "both" diff --git a/src/py/mat3ra/made/tools/enums.py b/src/py/mat3ra/made/tools/enums.py new file mode 100644 index 00000000..c525714e --- /dev/null +++ b/src/py/mat3ra/made/tools/enums.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class SurfaceTypes(str, Enum): + TOP = "top" + BOTTOM = "bottom" + BOTH = "both" From 35f40f4999569ddd896b48c7e335af87a3081569 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:09:08 -0700 Subject: [PATCH 75/89] chore: run lint fix and types --- src/py/mat3ra/made/tools/analyze.py | 2 +- src/py/mat3ra/made/tools/build/passivation/__init__.py | 10 ++++++++-- src/py/mat3ra/made/tools/build/passivation/builders.py | 5 +++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index 6b9ff0d0..a99cabd2 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -4,8 +4,8 @@ from scipy.spatial import cKDTree from ..material import Material -from .enums import SurfaceTypes 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 diff --git a/src/py/mat3ra/made/tools/build/passivation/__init__.py b/src/py/mat3ra/made/tools/build/passivation/__init__.py index b19e061d..c0b4b860 100644 --- a/src/py/mat3ra/made/tools/build/passivation/__init__.py +++ b/src/py/mat3ra/made/tools/build/passivation/__init__.py @@ -2,11 +2,17 @@ from mat3ra.made.material import Material from .configuration import PassivationConfiguration -from .builders import SurfacePassivationBuilder, UndercoordinationPassivationBuilder +from .builders import ( + SurfacePassivationBuilder, + UndercoordinationPassivationBuilder, + SurfacePassivationBuilderParameters, +) def create_passivation( configuration: PassivationConfiguration, - builder: Union[SurfacePassivationBuilder, UndercoordinationPassivationBuilder] = SurfacePassivationBuilder(), + builder: Union[SurfacePassivationBuilder, UndercoordinationPassivationBuilder, None] = None, ) -> Material: + if builder is None: + builder = SurfacePassivationBuilder(build_parameters=SurfacePassivationBuilderParameters()) return builder.get_material(configuration) diff --git a/src/py/mat3ra/made/tools/build/passivation/builders.py b/src/py/mat3ra/made/tools/build/passivation/builders.py index 134f611f..b06979e4 100644 --- a/src/py/mat3ra/made/tools/build/passivation/builders.py +++ b/src/py/mat3ra/made/tools/build/passivation/builders.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List import numpy as np from mat3ra.made.material import Material @@ -77,7 +77,8 @@ class SurfacePassivationBuilder(PassivationBuilder): Detects surface atoms looking along Z axis and passivates either the top or bottom surface or both. """ - build_parameters: Optional[SurfacePassivationBuilderParameters] = SurfacePassivationBuilderParameters() + build_parameters: SurfacePassivationBuilderParameters + _DefaultBuildParameters = SurfacePassivationBuilderParameters() _ConfigurationType = PassivationConfiguration def create_passivated_material(self, configuration: PassivationConfiguration) -> Material: From ddfc66179479f38cc0f744b8890cba6ae52d5926 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 9 Sep 2024 19:58:18 -0700 Subject: [PATCH 76/89] update: specify cart or cryst coords for add atom --- src/py/mat3ra/made/basis.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/py/mat3ra/made/basis.py b/src/py/mat3ra/made/basis.py index 44f4dd4f..0b0888ee 100644 --- a/src/py/mat3ra/made/basis.py +++ b/src/py/mat3ra/made/basis.py @@ -13,7 +13,7 @@ class Basis(RoundNumericValuesMixin, BaseModel): elements: ArrayWithIds = ArrayWithIds(values=["Si"]) coordinates: ArrayWithIds = ArrayWithIds(values=[0, 0, 0]) units: str = AtomicCoordinateUnits.crystal - cell: Optional[Cell] = None + cell: Cell = Cell() labels: Optional[ArrayWithIds] = ArrayWithIds(values=[]) constraints: Optional[ArrayWithIds] = ArrayWithIds(values=[]) @@ -76,12 +76,28 @@ def to_crystal(self): self.coordinates.map_array_in_place(self.cell.convert_point_to_crystal) self.units = AtomicCoordinateUnits.crystal - def add_atom(self, element="Si", coordinate=None, force=False): + def add_atom( + self, + element="Si", + coordinate: Optional[List[float]] = None, + use_cartesian_coordinates: bool = False, + force: bool = False, + ): if coordinate is None: coordinate = [0, 0, 0] - cartesian_coordinates = [self.cell.convert_point_to_cartesian(coord) for coord in self.coordinates.values] - cartesian_coordinate = self.cell.convert_point_to_cartesian(coordinate) - if get_overlapping_coordinates(cartesian_coordinate, cartesian_coordinates, threshold=0.01): + if use_cartesian_coordinates: + if self.is_in_crystal_units: + coordinate = self.cell.convert_point_to_crystal(coordinate) + else: + if self.is_in_cartesian_units: + coordinate = self.cell.convert_point_to_cartesian(coordinate) + cartesian_coordinates_for_overlap_check = [ + self.cell.convert_point_to_cartesian(coord) for coord in self.coordinates.values + ] + cartesian_coordinate_for_overlap_check = self.cell.convert_point_to_cartesian(coordinate) + if get_overlapping_coordinates( + cartesian_coordinate_for_overlap_check, cartesian_coordinates_for_overlap_check, threshold=0.01 + ): if force: print(f"Warning: Overlapping coordinates found for {coordinate}. Adding atom anyway.") else: From a20bb2d3a372092b7de44dc0e18c90c4482b2255 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 9 Sep 2024 20:01:06 -0700 Subject: [PATCH 77/89] chore: make simpler, i think --- src/py/mat3ra/made/basis.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/py/mat3ra/made/basis.py b/src/py/mat3ra/made/basis.py index 0b0888ee..84a7d6e3 100644 --- a/src/py/mat3ra/made/basis.py +++ b/src/py/mat3ra/made/basis.py @@ -85,12 +85,10 @@ def add_atom( ): if coordinate is None: coordinate = [0, 0, 0] - if use_cartesian_coordinates: - if self.is_in_crystal_units: - coordinate = self.cell.convert_point_to_crystal(coordinate) - else: - if self.is_in_cartesian_units: - coordinate = self.cell.convert_point_to_cartesian(coordinate) + if use_cartesian_coordinates and self.is_in_crystal_units: + coordinate = self.cell.convert_point_to_crystal(coordinate) + if not use_cartesian_coordinates and self.is_in_cartesian_units: + coordinate = self.cell.convert_point_to_cartesian(coordinate) cartesian_coordinates_for_overlap_check = [ self.cell.convert_point_to_cartesian(coord) for coord in self.coordinates.values ] From e281136c286abb0d8d2d8bbd143193d299645ef1 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 9 Sep 2024 20:07:51 -0700 Subject: [PATCH 78/89] update: add docstring --- src/py/mat3ra/made/basis.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/py/mat3ra/made/basis.py b/src/py/mat3ra/made/basis.py index 84a7d6e3..cf08c295 100644 --- a/src/py/mat3ra/made/basis.py +++ b/src/py/mat3ra/made/basis.py @@ -83,6 +83,17 @@ def add_atom( use_cartesian_coordinates: bool = False, force: bool = False, ): + """ + Add an atom to the basis. + + Before adding the atom at the specified coordinate, checks that no other atom is overlapping within a threshold. + + Args: + element (str): Element symbol of the atom to be added. + coordinate (List[float]): Coordinate of the atom to be added. + use_cartesian_coordinates (bool): Whether the coordinate is in Cartesian units (or crystal by default). + force (bool): Whether to force adding the atom even if it overlaps with another atom. + """ if coordinate is None: coordinate = [0, 0, 0] if use_cartesian_coordinates and self.is_in_crystal_units: From 51d878f20b51b884d22bc097dec23d6376d3cfee Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 9 Sep 2024 20:11:06 -0700 Subject: [PATCH 79/89] update: add link to voronoi --- src/py/mat3ra/made/tools/analyze.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index a99cabd2..c79a3cbd 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -279,6 +279,7 @@ def get_nearest_neighbors_atom_indices( coordinate (List[float]): The position to find neighbors for. tolerance (float): tolerance parameter for near-neighbor finding. Faces that are smaller than tol fraction of the largest face are not included in the tessellation. (default: 0.1). + as per: https://pymatgen.org/pymatgen.analysis.html#pymatgen.analysis.local_env.VoronoiNN cutoff (float): The cutoff radius for identifying neighbors, in angstroms. Returns: @@ -300,7 +301,6 @@ def get_nearest_neighbors_atom_indices( if site_index is None: structure.append("X", coordinate, validate_proximity=False) site_index = len(structure.sites) - 1 - remove_dummy_atom = True try: neighbors = voronoi_nn.get_nn_info(structure, site_index) From a42aea841b2b452952a8c4c706ba60f4da20c42a Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 9 Sep 2024 20:13:35 -0700 Subject: [PATCH 80/89] chore: try to explain shadowing --- src/py/mat3ra/made/tools/analyze.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze.py b/src/py/mat3ra/made/tools/analyze.py index c79a3cbd..7bf67b1f 100644 --- a/src/py/mat3ra/made/tools/analyze.py +++ b/src/py/mat3ra/made/tools/analyze.py @@ -360,9 +360,11 @@ def is_height_within_limits(z: float, z_extremum: float, depth: float, surface: return (z >= z_extremum - depth) if surface == SurfaceTypes.TOP else (z <= z_extremum + depth) -def shadowing_check(z: float, neighbors_indices: List[int], surface: SurfaceTypes, coordinates: np.ndarray) -> bool: +def is_shadowed_by_neighbors_from_surface( + z: float, neighbors_indices: List[int], surface: SurfaceTypes, coordinates: np.ndarray +) -> bool: """ - Check if the atom is shadowed by its neighbors from the surface. + Check if any one of the neighboring atoms shadow the atom from the surface by being closer to the specified surface. Args: z (float): The z-coordinate of the atom. @@ -406,7 +408,7 @@ def get_surface_atom_indices( for idx, (x, y, z) in enumerate(coordinates): if is_height_within_limits(z, z_extremum, depth, surface): neighbors_indices = kd_tree.query_ball_point([x, y, z], r=shadowing_radius) - if shadowing_check(z, neighbors_indices, surface, coordinates): + if is_shadowed_by_neighbors_from_surface(z, neighbors_indices, surface, coordinates): exposed_atoms_indices.append(ids[idx]) return exposed_atoms_indices From fa2c2698bea09e12fcd4a0c39d80058d5c589e4e Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 10 Sep 2024 10:16:01 -0700 Subject: [PATCH 81/89] chore: rename --- .../made/tools/build/passivation/__init__.py | 4 ++-- .../made/tools/build/passivation/builders.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/passivation/__init__.py b/src/py/mat3ra/made/tools/build/passivation/__init__.py index c0b4b860..32ee79e4 100644 --- a/src/py/mat3ra/made/tools/build/passivation/__init__.py +++ b/src/py/mat3ra/made/tools/build/passivation/__init__.py @@ -4,14 +4,14 @@ from .configuration import PassivationConfiguration from .builders import ( SurfacePassivationBuilder, - UndercoordinationPassivationBuilder, + CoordinationBasedPassivationBuilder, SurfacePassivationBuilderParameters, ) def create_passivation( configuration: PassivationConfiguration, - builder: Union[SurfacePassivationBuilder, UndercoordinationPassivationBuilder, None] = None, + builder: Union[SurfacePassivationBuilder, CoordinationBasedPassivationBuilder, None] = None, ) -> Material: if builder is None: builder = SurfacePassivationBuilder(build_parameters=SurfacePassivationBuilderParameters()) diff --git a/src/py/mat3ra/made/tools/build/passivation/builders.py b/src/py/mat3ra/made/tools/build/passivation/builders.py index b06979e4..e6c73f88 100644 --- a/src/py/mat3ra/made/tools/build/passivation/builders.py +++ b/src/py/mat3ra/made/tools/build/passivation/builders.py @@ -123,25 +123,25 @@ def _get_passivant_coordinates( return (np.array(surface_atoms_coordinates) + np.array(passivant_bond_vector_crystal)).tolist() -class UndercoordinationPassivationBuilderParameters(SurfacePassivationBuilderParameters): +class CoordinationBasedPassivationBuilderParameters(SurfacePassivationBuilderParameters): """ - Parameters for the UndercoordinationPassivationBuilder. + Parameters for the CoordinationPassivationBuilder. Args: - coordination_threshold (int): The coordination threshold for undercoordination. + coordination_threshold (int): The coordination number threshold for atom to be considered undercoordinated. """ coordination_threshold: int = 3 -class UndercoordinationPassivationBuilder(PassivationBuilder): +class CoordinationBasedPassivationBuilder(PassivationBuilder): """ - Builder for passivating material based on undercoordination of atoms. + Builder for passivating material based on coordination number of each atom. Detects atoms with coordination number below a threshold and passivates them. """ - _BuildParametersType = UndercoordinationPassivationBuilderParameters - _DefaultBuildParameters = UndercoordinationPassivationBuilderParameters() + _BuildParametersType = CoordinationBasedPassivationBuilderParameters + _DefaultBuildParameters = CoordinationBasedPassivationBuilderParameters() def create_passivated_material(self, configuration: PassivationConfiguration) -> Material: material = super().create_passivated_material(configuration) From c7bac592d5c8a7fd3e2c7b52bf34a39ce7c3ac07 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:16:06 -0700 Subject: [PATCH 82/89] update: use cartesians to make easier to understand --- .../mat3ra/made/tools/build/defect/builders.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/defect/builders.py b/src/py/mat3ra/made/tools/build/defect/builders.py index 63d35a1c..34d3bd70 100644 --- a/src/py/mat3ra/made/tools/build/defect/builders.py +++ b/src/py/mat3ra/made/tools/build/defect/builders.py @@ -30,8 +30,7 @@ get_closest_site_id_from_coordinate_and_element, ) from ....utils import get_center_of_coordinates -from ...utils import transform_coordinate_to_supercell -from ...utils import coordinate as CoordinateCondition +from ...utils import transform_coordinate_to_supercell, coordinate as CoordinateCondition from ..utils import merge_materials from ..slab import SlabConfiguration, create_slab, Termination from ..supercell import create_supercell @@ -415,23 +414,26 @@ def create_island( The material with the island added. """ new_material = material.clone() - original_max_z = get_atomic_coordinates_extremum(new_material, use_cartesian_coordinates=False) + original_max_z = get_atomic_coordinates_extremum(new_material, use_cartesian_coordinates=True) material_with_additional_layers = self.create_material_with_additional_layers(new_material, thickness) - added_layers_max_z = get_atomic_coordinates_extremum(material_with_additional_layers) + added_layers_max_z = get_atomic_coordinates_extremum( + material_with_additional_layers, use_cartesian_coordinates=True + ) if condition is None: condition = self._default_condition - thickness_nudge_value = (added_layers_max_z - original_max_z) / thickness atoms_within_island = filter_by_condition_on_coordinates( material=material_with_additional_layers, condition=condition, use_cartesian_coordinates=use_cartesian_coordinates, ) - # Filter atoms in the added layers + # Filter atoms in the added layers between the original and added layers + a, b, c = material.lattice.a, material.lattice.b, material.lattice.c island_material = filter_by_box( material=atoms_within_island, - min_coordinate=[0, 0, original_max_z - thickness_nudge_value], - max_coordinate=[1, 1, added_layers_max_z], + min_coordinate=[0, 0, original_max_z], + max_coordinate=[a, b, added_layers_max_z], + use_cartesian_coordinates=True, ) return self.merge_slab_and_defect(island_material, new_material) From add238c67df81472645f81cca61efdb91fd21e1f Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:19:57 -0700 Subject: [PATCH 83/89] chore: remove unused --- src/py/mat3ra/made/tools/build/passivation/enums.py | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 src/py/mat3ra/made/tools/build/passivation/enums.py diff --git a/src/py/mat3ra/made/tools/build/passivation/enums.py b/src/py/mat3ra/made/tools/build/passivation/enums.py deleted file mode 100644 index feea29ef..00000000 --- a/src/py/mat3ra/made/tools/build/passivation/enums.py +++ /dev/null @@ -1,7 +0,0 @@ -# TODO: Get this from periodic table - -BOND_LENGTHS_MAP = { - ("C", "H"): 1.09, - ("Ni", "H"): 1.09, - ("Si", "H"): 1.48, -} From 70e571640a798ed7a3a6f5c4136a47809f2d2ffb Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:54:57 -0700 Subject: [PATCH 84/89] chore: add description of depth for passivation --- src/py/mat3ra/made/tools/build/passivation/builders.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/passivation/builders.py b/src/py/mat3ra/made/tools/build/passivation/builders.py index e6c73f88..696594e9 100644 --- a/src/py/mat3ra/made/tools/build/passivation/builders.py +++ b/src/py/mat3ra/made/tools/build/passivation/builders.py @@ -59,11 +59,13 @@ def _add_passivant_atoms(self, material: Material, coordinates: list, passivant: class SurfacePassivationBuilderParameters(BaseModel): """ - Parameters for the SurfacePassivationBuilder. + Parameters for the SurfacePassivationBuilder, defining how atoms near the surface are + detected and passivated. Args: - shadowing_radius (float): Radius for atoms shadowing underlying from passivation, in Angstroms. - depth (float): Depth from the top to look for exposed surface atoms to passivate, in Angstroms. + shadowing_radius (float): Radius around each surface atom to exclude underlying atoms from passivation. + depth (float): Depth from the topmost (or bottommost) atom into the material to consider for passivation, + accounting for features like islands, adatoms, and terraces. """ shadowing_radius: float = 2.5 From 943d315523fd2c322f252ef62326439985bd0c89 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:58:28 -0700 Subject: [PATCH 85/89] chore: run lint fix --- src/py/mat3ra/made/tools/build/defect/builders.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/defect/builders.py b/src/py/mat3ra/made/tools/build/defect/builders.py index 34d3bd70..637562bf 100644 --- a/src/py/mat3ra/made/tools/build/defect/builders.py +++ b/src/py/mat3ra/made/tools/build/defect/builders.py @@ -428,11 +428,10 @@ def create_island( use_cartesian_coordinates=use_cartesian_coordinates, ) # Filter atoms in the added layers between the original and added layers - a, b, c = material.lattice.a, material.lattice.b, material.lattice.c island_material = filter_by_box( material=atoms_within_island, min_coordinate=[0, 0, original_max_z], - max_coordinate=[a, b, added_layers_max_z], + max_coordinate=[material.lattice.a, material.lattice.b, added_layers_max_z], use_cartesian_coordinates=True, ) From 99aca0a67446af88c4a560970ba19a202654d1b5 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:20:58 -0700 Subject: [PATCH 86/89] test: print island --- tests/py/unit/test_tools_build_defect.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/py/unit/test_tools_build_defect.py b/tests/py/unit/test_tools_build_defect.py index 8ba45e0d..cd97eff2 100644 --- a/tests/py/unit/test_tools_build_defect.py +++ b/tests/py/unit/test_tools_build_defect.py @@ -125,6 +125,7 @@ def test_create_island(): ) defect = create_slab_defect(configuration=island_config, builder=IslandSlabDefectBuilder()) + print(defect.to_json()) # Only 2 atoms in the island were added for this configuration NUMBER_OF_ATOMS_IN_ISLAND = 2 From 6bd32c70d3450998ad4282c096c342adce98b8c2 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:26:15 -0700 Subject: [PATCH 87/89] test: print island --- tests/py/unit/test_tools_build_defect.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/py/unit/test_tools_build_defect.py b/tests/py/unit/test_tools_build_defect.py index cd97eff2..a342e151 100644 --- a/tests/py/unit/test_tools_build_defect.py +++ b/tests/py/unit/test_tools_build_defect.py @@ -124,6 +124,8 @@ def test_create_island(): thickness=1, ) + print(SLAB_111.to_json()) + defect = create_slab_defect(configuration=island_config, builder=IslandSlabDefectBuilder()) print(defect.to_json()) From 8da0d5e9792cc6dedcb4753ef70646dc319f8299 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 10 Sep 2024 15:43:36 -0700 Subject: [PATCH 88/89] update: fix test by using 001 si slab since this doesnt have any discreapncies between Python and Pyodide results --- tests/py/unit/test_tools_build_defect.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/py/unit/test_tools_build_defect.py b/tests/py/unit/test_tools_build_defect.py index a342e151..b8153cab 100644 --- a/tests/py/unit/test_tools_build_defect.py +++ b/tests/py/unit/test_tools_build_defect.py @@ -115,23 +115,20 @@ def test_create_crystal_site_adatom(): def test_create_island(): condition = CoordinateCondition.CylinderCoordinateCondition( - center_position=[0.625, 0.5], radius=0.25, min_z=0, max_z=1 + center_position=[0.5, 0.5], radius=0.15, min_z=0, max_z=1 ) island_config = IslandSlabDefectConfiguration( - crystal=SLAB_111, + crystal=SLAB_001, defect_type="island", condition=condition, - thickness=1, + number_of_added_layers=1, ) - print(SLAB_111.to_json()) - defect = create_slab_defect(configuration=island_config, builder=IslandSlabDefectBuilder()) - print(defect.to_json()) - # Only 2 atoms in the island were added for this configuration - NUMBER_OF_ATOMS_IN_ISLAND = 2 - assert len(defect.basis.elements.values) == len(SLAB_111.basis.elements.values) + NUMBER_OF_ATOMS_IN_ISLAND + # Only 1 atoms in the island were added for this configuration with 001 slab orientation + NUMBER_OF_ATOMS_IN_ISLAND = 1 + assert len(defect.basis.elements.values) == len(SLAB_001.basis.elements.values) + NUMBER_OF_ATOMS_IN_ISLAND assert defect.basis.elements.values[-1] == "Si" From 3e89a8fde1a6ab385c6fe891216d131345f8b27c Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:35:03 -0700 Subject: [PATCH 89/89] update: rename function --- src/py/mat3ra/made/tools/build/passivation/builders.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/passivation/builders.py b/src/py/mat3ra/made/tools/build/passivation/builders.py index 696594e9..dd383d04 100644 --- a/src/py/mat3ra/made/tools/build/passivation/builders.py +++ b/src/py/mat3ra/made/tools/build/passivation/builders.py @@ -197,9 +197,9 @@ def _get_passivant_coordinates( return passivant_coordinates - def get_coordination_numbers(self, material: Material): + def get_unique_coordination_numbers(self, material: Material): """ - Get the coordination numbers for all atoms in the material. + Get unique coordination numbers for all atoms in the material for current builder parameters. Args: material (Material): The material object. @@ -211,5 +211,4 @@ def get_coordination_numbers(self, material: Material): coordination_numbers = set( get_coordination_numbers(material=material, cutoff=self.build_parameters.shadowing_radius) ) - print("coordination numbers:", coordination_numbers) return coordination_numbers