Skip to content

Commit

Permalink
Merge branch 'main' into feature/SOF-7425
Browse files Browse the repository at this point in the history
  • Loading branch information
VsevolodX committed Sep 11, 2024
2 parents 8b98fa6 + 910c6a2 commit 37257b5
Show file tree
Hide file tree
Showing 11 changed files with 170 additions and 116 deletions.
33 changes: 30 additions & 3 deletions src/py/mat3ra/made/basis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=[])

Expand Down Expand Up @@ -76,10 +76,37 @@ 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,
):
"""
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 get_overlapping_coordinates(coordinate, self.coordinates.values, threshold=0.01):
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
]
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:
Expand Down
108 changes: 68 additions & 40 deletions src/py/mat3ra/made/tools/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
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 .enums import SurfaceTypes
from .third_party import ASEAtoms, PymatgenIStructure, PymatgenVoronoiNN
from .utils import decorator_handle_periodic_boundary_conditions

Expand Down Expand Up @@ -277,7 +277,10 @@ 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).
as per: https://pymatgen.org/pymatgen.analysis.html#pymatgen.analysis.local_env.VoronoiNN
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.
Expand All @@ -298,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)
Expand Down Expand Up @@ -342,51 +344,37 @@ def get_atomic_coordinates_extremum(
return getattr(np, extremum)(values)


def get_local_extremum_atom_index(
material: Material,
coordinate: List[float],
extremum: Literal["max", "min"] = "max",
vicinity: float = 1.0,
use_cartesian_coordinates: bool = False,
) -> int:
def is_height_within_limits(z: float, z_extremum: float, depth: float, surface: SurfaceTypes) -> bool:
"""
Return the id of the atom with the minimum or maximum z-coordinate
within a certain vicinity of a given (x, y) coordinate.
Check if the height of an atom is within the specified limits.
Args:
material (Material): Material object.
coordinate (List[float]): (x, y, z) coordinate to find the local extremum.
extremum (str): "min" or "max".
vicinity (float): Radius of the vicinity, in Angstroms.
use_cartesian_coordinates (bool): Whether to use Cartesian coordinates.
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:
int: id of the atom with the minimum or maximum z-coordinate.
bool: True if the height is within the limits, False otherwise.
"""
new_material = material.clone()
new_material.to_cartesian()
if not use_cartesian_coordinates:
coordinate = new_material.basis.cell.convert_point_to_cartesian(coordinate)

coordinates = np.array(new_material.basis.coordinates.values)
ids = np.array(new_material.basis.coordinates.ids)
tree = cKDTree(coordinates[:, :2])
indices = tree.query_ball_point(coordinate[:2], vicinity)
z_values = [(id, coord[2]) for id, coord in zip(ids[indices], coordinates[indices])]

if extremum == "max":
extremum_z_atom = max(z_values, key=lambda item: item[1])
else:
extremum_z_atom = min(z_values, key=lambda item: item[1])

return extremum_z_atom[0]
return (z >= z_extremum - depth) if surface == SurfaceTypes.TOP else (z <= z_extremum + depth)


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 is_shadowed_by_neighbors_from_surface(
z: float, neighbors_indices: List[int], surface: SurfaceTypes, coordinates: np.ndarray
) -> bool:
"""
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.
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.
def shadowing_check(z: float, neighbors_indices: List[int], surface: SurfaceTypes, coordinates: np.ndarray):
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
)
Expand Down Expand Up @@ -418,9 +406,9 @@ 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):
if is_shadowed_by_neighbors_from_surface(z, neighbors_indices, surface, coordinates):
exposed_atoms_indices.append(ids[idx])

return exposed_atoms_indices
Expand Down Expand Up @@ -481,3 +469,43 @@ def get_undercoordinated_atom_indices(
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


def get_local_extremum_atom_index(
material: Material,
coordinate: List[float],
extremum: Literal["max", "min"] = "max",
vicinity: float = 1.0,
use_cartesian_coordinates: bool = False,
) -> int:
"""
Return the id of the atom with the minimum or maximum z-coordinate
within a certain vicinity of a given (x, y) coordinate.
Args:
material (Material): Material object.
coordinate (List[float]): (x, y, z) coordinate to find the local extremum atom index for.
extremum (str): "min" or "max".
vicinity (float): Radius of the vicinity, in Angstroms.
use_cartesian_coordinates (bool): Whether to use Cartesian coordinates.
Returns:
int: id of the atom with the minimum or maximum z-coordinate.
"""
new_material = material.clone()
new_material.to_cartesian()
if not use_cartesian_coordinates:
coordinate = new_material.basis.cell.convert_point_to_cartesian(coordinate)

coordinates = np.array(new_material.basis.coordinates.values)
ids = np.array(new_material.basis.coordinates.ids)
tree = cKDTree(coordinates[:, :2])
indices = tree.query_ball_point(coordinate[:2], vicinity)
z_values = [(id, coord[2]) for id, coord in zip(ids[indices], coordinates[indices])]

if extremum == "max":
extremum_z_atom = max(z_values, key=lambda item: item[1])
else:
extremum_z_atom = min(z_values, key=lambda item: item[1])

return extremum_z_atom[0]
17 changes: 9 additions & 8 deletions src/py/mat3ra/made/tools/build/defect/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -415,23 +414,25 @@ 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
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=[material.lattice.a, material.lattice.b, added_layers_max_z],
use_cartesian_coordinates=True,
)

return self.merge_slab_and_defect(island_material, new_material)
Expand Down
18 changes: 18 additions & 0 deletions src/py/mat3ra/made/tools/build/passivation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import Union

from mat3ra.made.material import Material
from .configuration import PassivationConfiguration
from .builders import (
SurfacePassivationBuilder,
CoordinationBasedPassivationBuilder,
SurfacePassivationBuilderParameters,
)


def create_passivation(
configuration: PassivationConfiguration,
builder: Union[SurfacePassivationBuilder, CoordinationBasedPassivationBuilder, None] = None,
) -> Material:
if builder is None:
builder = SurfacePassivationBuilder(build_parameters=SurfacePassivationBuilderParameters())
return builder.get_material(configuration)
32 changes: 17 additions & 15 deletions src/py/mat3ra/made/tools/build/passivation/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
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,
Expand Down Expand Up @@ -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
Expand All @@ -77,7 +79,8 @@ 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: SurfacePassivationBuilderParameters
_DefaultBuildParameters = SurfacePassivationBuilderParameters()
_ConfigurationType = PassivationConfiguration

def create_passivated_material(self, configuration: PassivationConfiguration) -> Material:
Expand Down Expand Up @@ -122,25 +125,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)
Expand Down Expand Up @@ -194,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.
Expand All @@ -208,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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from mat3ra.made.material import Material

from .enums import SurfaceTypes
from ...enums import SurfaceTypes
from ...build import BaseConfiguration


Expand Down
14 changes: 0 additions & 14 deletions src/py/mat3ra/made/tools/build/passivation/enums.py

This file was deleted.

7 changes: 7 additions & 0 deletions src/py/mat3ra/made/tools/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from enum import Enum


class SurfaceTypes(str, Enum):
TOP = "top"
BOTTOM = "bottom"
BOTH = "both"
Loading

0 comments on commit 37257b5

Please sign in to comment.