Skip to content

Commit

Permalink
Merge pull request #148 from Exabyte-io/feature/SOF-7393-1
Browse files Browse the repository at this point in the history
feature/SOF 7393 1
  • Loading branch information
VsevolodX authored Jul 21, 2024
2 parents b5634b6 + 629d822 commit cb15084
Show file tree
Hide file tree
Showing 11 changed files with 268 additions and 19 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
*.gz
*.whl
MANIFEST

# PyInstaller
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,15 @@ Conventions:

- The "tools" module has external dependencies on "pymatgen" and "ase" packages and so is meant as optional. When implementing new functionality, the use of ASE is recommended over pymatgen for compatibility purposes.

#### 5.2.3. Developing locally for pyodide

To build and serve locally, use the following command:

```bash
wheel_server
```
More details can be found in the [script documentation](https://github.com/Exabyte-io/utils/blob/main/README.md).

### 5.3. Known Issues

#### 5.3.1. JavaScript/TypeScript
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ all = [

# Entrypoint scripts can be defined here, see examples below.
[project.scripts]
# my-script = "my_package.my_module:my_function"
# To proxy the wheel_server script from mat3ra-utils for development purposes
wheel_server = "mat3ra.utils.wheel_server:main"


[build-system]
Expand Down
12 changes: 9 additions & 3 deletions src/py/mat3ra/made/tools/build/defect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
AdatomSlabDefectBuilder,
EquidistantAdatomSlabDefectBuilder,
CrystalSiteAdatomSlabDefectBuilder,
IslandSlabDefectBuilder,
)
from .configuration import PointDefectConfiguration, AdatomSlabPointDefectConfiguration
from .configuration import PointDefectConfiguration, AdatomSlabPointDefectConfiguration, IslandSlabDefectConfiguration
from .enums import PointDefectTypeEnum


Expand Down Expand Up @@ -43,9 +44,14 @@ def create_defect(


def create_slab_defect(
configuration: Union[AdatomSlabPointDefectConfiguration],
configuration: Union[AdatomSlabPointDefectConfiguration, IslandSlabDefectConfiguration],
builder: Optional[
Union[AdatomSlabDefectBuilder, EquidistantAdatomSlabDefectBuilder, CrystalSiteAdatomSlabDefectBuilder]
Union[
AdatomSlabDefectBuilder,
EquidistantAdatomSlabDefectBuilder,
CrystalSiteAdatomSlabDefectBuilder,
IslandSlabDefectBuilder,
]
] = None,
) -> Material:
"""
Expand Down
67 changes: 65 additions & 2 deletions src/py/mat3ra/made/tools/build/defect/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
PymatgenInterstitial,
)

from ...modify import add_vacuum, filter_material_by_ids
from ...modify import (
add_vacuum,
filter_material_by_ids,
filter_by_box,
filter_by_condition_on_coordinates,
)
from ...build import BaseBuilder
from ...convert import to_pymatgen
from ...analyze import (
Expand All @@ -26,7 +31,7 @@
from ..slab import SlabConfiguration, create_slab, Termination
from ..supercell import create_supercell
from ..mixins import ConvertGeneratedItemsPymatgenStructureMixin
from .configuration import PointDefectConfiguration, AdatomSlabPointDefectConfiguration
from .configuration import PointDefectConfiguration, AdatomSlabPointDefectConfiguration, IslandSlabDefectConfiguration


class PointDefectBuilderParameters(BaseModel):
Expand Down Expand Up @@ -308,3 +313,61 @@ def create_adatom(
)

return [self.merge_slab_and_defect(new_material, only_adatom_material)]


class IslandSlabDefectBuilder(SlabDefectBuilder):
_ConfigurationType: type(IslandSlabDefectConfiguration) = IslandSlabDefectConfiguration # type: ignore
_GeneratedItemType: Material = Material

def create_island(
self,
material: Material,
condition: Optional[Callable[[List[float]], bool]] = None,
thickness: int = 1,
use_cartesian_coordinates: bool = False,
) -> List[Material]:
"""
Create an island at the specified position on the surface of the material.
Args:
material: The material to add the island to.
condition: The condition on coordinates to describe the island.
thickness: The thickness of the island in layers.
use_cartesian_coordinates: Whether to use Cartesian coordinates for the condition.
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:

def condition(coordinate: List[float]):
return True

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],
max_coordinate=[1, 1, added_layers_max_z],
)

return [self.merge_slab_and_defect(island_material, new_material)]

def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]:
condition_callable, _ = configuration.condition
return self.create_island(
material=configuration.crystal,
condition=condition_callable,
thickness=configuration.thickness,
use_cartesian_coordinates=configuration.use_cartesian_coordinates,
)
80 changes: 75 additions & 5 deletions src/py/mat3ra/made/tools/build/defect/configuration.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
from typing import Optional, List, Any
from typing import Optional, List, Any, Callable, Dict, Tuple
from pydantic import BaseModel

from mat3ra.code.entity import InMemoryEntity
from mat3ra.made.material import Material

from ...analyze import get_closest_site_id_from_coordinate, get_atomic_coordinates_extremum
from .enums import PointDefectTypeEnum
from ...utils import CoordinateConditionBuilder
from .enums import PointDefectTypeEnum, SlabDefectTypeEnum


class BaseDefectConfiguration(BaseModel):
# TODO: fix arbitrary_types_allowed error and set Material class type
crystal: Any = None

@property
def _json(self):
return {
"type": "BaseDefectConfiguration",
"crystal": self.crystal.to_json(),
}


class PointDefectConfiguration(BaseDefectConfiguration, InMemoryEntity):
"""
Configuration for a point defect.
Args:
crystal (Material): The Material object.
defect_type (PointDefectTypeEnum): The type of the defect.
coordinate (List[float]): The crystal coordinate of the defect.
chemical_element (Optional[str]): The chemical element.
"""

defect_type: PointDefectTypeEnum
coordinate: List[float] = [0, 0, 0] # fractional coordinates
coordinate: List[float] = [0, 0, 0] # crystal coordinates
chemical_element: Optional[str] = None

@classmethod
Expand Down Expand Up @@ -45,8 +63,8 @@ def from_approximate_position(
@property
def _json(self):
return {
**super()._json,
"type": "PointDefectConfiguration",
"crystal": self.crystal.to_json(),
"defect_type": self.defect_type.name,
"coordinate": self.coordinate,
"chemical_element": self.chemical_element,
Expand All @@ -58,8 +76,20 @@ class SlabDefectConfiguration(BaseDefectConfiguration, InMemoryEntity):


class SlabPointDefectConfiguration(SlabDefectConfiguration, PointDefectConfiguration):
"""
Configuration for a slab point defect.
Args:
crystal (Material): The Material object.
defect_type (PointDefectTypeEnum): The type of the defect.
coordinate (List[float]): The crystal coordinate of the defect.
chemical_element (Optional[str]): The chemical element.
position_on_surface (List[float]): The position on the surface in 2D crystal coordinates.
distance_z (float): The distance in z direction in angstroms.
"""

position_on_surface: List[float]
distance_z: float
distance_z: float # in angstroms

def __init__(self, **data):
super().__init__(**data)
Expand All @@ -85,6 +115,18 @@ def _json(self):


class AdatomSlabPointDefectConfiguration(SlabPointDefectConfiguration):
"""
Configuration for an adatom slab point defect.
Args:
crystal (Material): The Material object.
defect_type (PointDefectTypeEnum): The type of the defect.
coordinate (List[float]): The crystal coordinate of the defect.
chemical_element (Optional[str]): The chemical element.
position_on_surface (List[float]): The position on the surface in 2D crystal coordinates.
distance_z (float): The distance in z direction in angstroms.
"""

defect_type: PointDefectTypeEnum = PointDefectTypeEnum.ADATOM

@property
Expand All @@ -93,3 +135,31 @@ def _json(self):
**super()._json,
"type": "AdatomSlabPointDefectConfiguration",
}


class IslandSlabDefectConfiguration(SlabDefectConfiguration):
"""
Configuration for an island slab defect.
Args:
crystal (Material): The Material object.
defect_type (SlabDefectTypeEnum): The type of the defect.
condition (Optional[Tuple[Callable[[List[float]], bool], Dict]]): The condition on coordinates
to shape the island. Defaults to a cylinder.
thickness (int): The thickness of the defect in atomic layers.
"""

defect_type: SlabDefectTypeEnum = SlabDefectTypeEnum.ISLAND
condition: Optional[Tuple[Callable[[List[float]], bool], Dict]] = CoordinateConditionBuilder().cylinder()
thickness: int = 1 # in atomic layers

@property
def _json(self):
_, condition_json = self.condition
return {
**super()._json,
"type": "IslandSlabDefectConfiguration",
"defect_type": self.defect_type.name,
"condition": condition_json,
"thickness": self.thickness,
}
4 changes: 4 additions & 0 deletions src/py/mat3ra/made/tools/build/defect/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ class PointDefectTypeEnum(str, Enum):
SUBSTITUTION = "substitution"
INTERSTITIAL = "interstitial"
ADATOM = "adatom"


class SlabDefectTypeEnum(str, Enum):
ISLAND = "island"
8 changes: 6 additions & 2 deletions src/py/mat3ra/made/tools/build/slab/configuration.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import List, Tuple, Any

import numpy as np
from pydantic import BaseModel

from mat3ra.code.entity import InMemoryEntity
Expand Down Expand Up @@ -28,7 +30,7 @@ class SlabConfiguration(BaseModel, InMemoryEntity):
miller_indices: Tuple[int, int, int] = (0, 0, 1)
thickness: int = 1
vacuum: int = 1
xy_supercell_matrix: List[List[int]] = [[1, 0], [0, 1]]
xy_supercell_matrix: List[List[int]] = np.eye(2).tolist()
use_conventional_cell: bool = True
use_orthogonal_z: bool = False
make_primitive: bool = False
Expand All @@ -39,11 +41,13 @@ def __init__(
miller_indices=miller_indices,
thickness=thickness,
vacuum=vacuum,
xy_supercell_matrix=xy_supercell_matrix,
xy_supercell_matrix=None,
use_conventional_cell=use_conventional_cell,
use_orthogonal_z=use_orthogonal_z,
make_primitive=make_primitive,
):
if xy_supercell_matrix is None:
xy_supercell_matrix = np.eye(2).tolist()
bulk = bulk or Material(Material.default_config)
__bulk_pymatgen_structure = (
PymatgenSpacegroupAnalyzer(to_pymatgen(bulk)).get_conventional_standard_structure()
Expand Down
71 changes: 70 additions & 1 deletion src/py/mat3ra/made/tools/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from functools import wraps
from typing import Callable, List, Optional
from typing import Callable, Dict, List, Optional, Tuple

import numpy as np
from mat3ra.utils.matrix import convert_2x2_to_3x3
Expand Down Expand Up @@ -89,6 +89,23 @@ def is_coordinate_in_cylinder(
)


def is_coordinate_in_sphere(coordinate: List[float], center_position: List[float], radius: float = 0.25) -> bool:
"""
Check if a coordinate is inside a sphere.
Args:
coordinate (List[float]): The coordinate to check.
center_position (List[float]): The coordinates of the center position.
radius (float): The radius of the sphere.
Returns:
bool: True if the coordinate is inside the sphere, False otherwise.
"""
np_coordinate = np.array(coordinate)
np_center_position = np.array(center_position)
distance_squared = np.sum((np_coordinate - np_center_position) ** 2)
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:
Expand Down Expand Up @@ -215,3 +232,55 @@ def transform_coordinate_to_supercell(
if reverse:
converted_array = (np_coordinate - np_translation_vector) * np_scaling_factor
return converted_array.tolist()


class CoordinateConditionBuilder:
def create_condition(self, condition_type: str, evaluation_func: Callable, **kwargs) -> Tuple[Callable, Dict]:
condition_json = {"type": condition_type, **kwargs}
return lambda coordinate: evaluation_func(coordinate, **kwargs), condition_json

def cylinder(
self, center_position: List[float] = [0.5, 0.5], radius: float = 0.25, min_z: float = 0, max_z: float = 1
):
return self.create_condition(
condition_type="cylinder",
evaluation_func=is_coordinate_in_cylinder,
center_position=center_position,
radius=radius,
min_z=min_z,
max_z=max_z,
)

def sphere(self, center_position: List[float] = [0.5, 0.5, 0.5], radius: float = 0.25):
return self.create_condition(
condition_type="sphere",
evaluation_func=is_coordinate_in_sphere,
center_position=center_position,
radius=radius,
)

def prism(
self,
coordinate_1: List[float] = [0, 0],
coordinate_2: List[float] = [1, 0],
coordinate_3: List[float] = [0, 1],
min_z: float = 0,
max_z: float = 1,
):
return self.create_condition(
condition_type="prism",
evaluation_func=is_coordinate_in_triangular_prism,
coordinate_1=coordinate_1,
coordinate_2=coordinate_2,
coordinate_3=coordinate_3,
min_z=min_z,
max_z=max_z,
)

def box(self, min_coordinate: List[float] = [0, 0, 0], max_coordinate: List[float] = [1, 1, 1]):
return self.create_condition(
condition_type="box",
evaluation_func=is_coordinate_in_box,
min_coordinate=min_coordinate,
max_coordinate=max_coordinate,
)
Loading

0 comments on commit cb15084

Please sign in to comment.