Skip to content

Commit

Permalink
Merge pull request #151 from Exabyte-io/feature/SOF-7397
Browse files Browse the repository at this point in the history
feature/SOF 7397
  • Loading branch information
VsevolodX authored Aug 12, 2024
2 parents 8077069 + 4c6a391 commit 361e423
Show file tree
Hide file tree
Showing 20 changed files with 618 additions and 220 deletions.
4 changes: 2 additions & 2 deletions src/py/mat3ra/made/basis.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ def from_dict(
coordinates: List[Dict],
units: str,
labels: Optional[List[Dict]] = None,
cell: Optional[Dict] = None,
cell: Optional[List[List[float]]] = None,
constraints: Optional[List[Dict]] = None,
) -> "Basis":
return Basis(
elements=ArrayWithIds.from_list_of_dicts(elements),
coordinates=ArrayWithIds.from_list_of_dicts(coordinates),
units=units,
cell=Cell.from_nested_array(cell),
cell=Cell.from_vectors_array(cell),
labels=ArrayWithIds.from_list_of_dicts(labels) if labels else ArrayWithIds(values=[]),
constraints=ArrayWithIds.from_list_of_dicts(constraints) if constraints else ArrayWithIds(values=[]),
)
Expand Down
32 changes: 16 additions & 16 deletions src/py/mat3ra/made/cell.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List
from typing import List, Optional

import numpy as np
from mat3ra.utils.mixins import RoundNumericValuesMixin
Expand All @@ -13,13 +13,13 @@ class Cell(RoundNumericValuesMixin, BaseModel):
__round_precision__ = 6

@classmethod
def from_nested_array(cls, nested_array):
if nested_array is None:
nested_array = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
return cls(vector1=nested_array[0], vector2=nested_array[1], vector3=nested_array[2])
def from_vectors_array(cls, vectors_array: Optional[List[List[float]]] = None) -> "Cell":
if vectors_array is None:
vectors_array = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
return cls(vector1=vectors_array[0], vector2=vectors_array[1], vector3=vectors_array[2])

@property
def vectors_as_nested_array(self, skip_rounding=False) -> List[List[float]]:
def vectors_as_array(self, skip_rounding=False) -> List[List[float]]:
if skip_rounding:
return [self.vector1, self.vector2, self.vector3]
return self.round_array_or_number([self.vector1, self.vector2, self.vector3])
Expand All @@ -32,22 +32,22 @@ def to_json(self, skip_rounding=False):
self.vector3 if skip_rounding else _(self.vector3),
]

def clone(self):
return self.from_nested_array(self.vectors_as_nested_array)
def clone(self) -> "Cell":
return self.from_vectors_array(self.vectors_as_array)

def clone_and_scale_by_matrix(self, matrix):
def clone_and_scale_by_matrix(self, matrix: List[List[float]]) -> "Cell":
new_cell = self.clone()
new_cell.scale_by_matrix(matrix)
return new_cell

def convert_point_to_cartesian(self, point):
np_vector = np.array(self.vectors_as_nested_array)
def convert_point_to_cartesian(self, point: List[float]) -> List[float]:
np_vector = np.array(self.vectors_as_array)
return np.dot(point, np_vector)

def convert_point_to_crystal(self, point):
np_vector = np.array(self.vectors_as_nested_array)
def convert_point_to_crystal(self, point: List[float]) -> List[float]:
np_vector = np.array(self.vectors_as_array)
return np.dot(point, np.linalg.inv(np_vector))

def scale_by_matrix(self, matrix):
np_vector = np.array(self.vectors_as_nested_array)
self.vector1, self.vector2, self.vector3 = np.dot(matrix, np_vector).tolist()
def scale_by_matrix(self, matrix: List[List[float]]):
np_vector = np.array(self.vectors_as_array)
self.vector1, self.vector2, self.vector3 = np.dot(np.array(matrix), np_vector).tolist()
34 changes: 28 additions & 6 deletions src/py/mat3ra/made/lattice.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from .cell import Cell

HASH_TOLERANCE = 3
DEFAULT_UNITS = {"length": "angstrom", "angle": "degree"}
DEFAULT_TYPE = "TRI"


class Lattice(RoundNumericValuesMixin, BaseModel):
Expand All @@ -17,11 +19,8 @@ class Lattice(RoundNumericValuesMixin, BaseModel):
alpha: float = 90.0
beta: float = 90.0
gamma: float = 90.0
units: Dict[str, str] = {
"length": "angstrom",
"angle": "degree",
}
type: str = "TRI"
units: Dict[str, str] = DEFAULT_UNITS
type: str = DEFAULT_TYPE

@property
def vectors(self) -> List[List[float]]:
Expand Down Expand Up @@ -52,6 +51,29 @@ def vectors(self) -> List[List[float]]:
[0.0, 0.0, c],
]

@classmethod
def from_vectors_array(
cls, vectors: List[List[float]], units: Optional[Dict[str, str]] = None, type: Optional[str] = None
) -> "Lattice":
"""
Create a Lattice object from a nested array of vectors.
Args:
vectors (List[List[float]]): A nested array of vectors.
Returns:
Lattice: A Lattice object.
"""
a = np.linalg.norm(vectors[0])
b = np.linalg.norm(vectors[1])
c = np.linalg.norm(vectors[2])
alpha = np.degrees(np.arccos(np.dot(vectors[1], vectors[2]) / (b * c)))
beta = np.degrees(np.arccos(np.dot(vectors[0], vectors[2]) / (a * c)))
gamma = np.degrees(np.arccos(np.dot(vectors[0], vectors[1]) / (a * b)))
if units is None:
units = DEFAULT_UNITS
if type is None:
type = DEFAULT_TYPE
return cls(a=float(a), b=float(b), c=float(c), alpha=alpha, beta=beta, gamma=gamma, units=units, type=type)

def to_json(self, skip_rounding: bool = False) -> Dict[str, Any]:
__round__ = RoundNumericValuesMixin.round_array_or_number
round_func = __round__ if not skip_rounding else lambda x: x
Expand All @@ -78,7 +100,7 @@ def vector_arrays(self) -> List[List[float]]:

@property
def cell(self) -> Cell:
return Cell.from_nested_array(self.vector_arrays)
return Cell.from_vectors_array(self.vector_arrays)

def volume(self) -> float:
np_vector = np.array(self.vector_arrays)
Expand Down
5 changes: 5 additions & 0 deletions src/py/mat3ra/made/material.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,8 @@ def to_crystal(self) -> None:
new_basis = self.basis.copy()
new_basis.to_crystal()
self.basis = new_basis

def set_coordinates(self, coordinates: List[List[float]]) -> None:
new_basis = self.basis.copy()
new_basis.coordinates.values = coordinates
self.basis = new_basis
13 changes: 7 additions & 6 deletions src/py/mat3ra/made/tools/build/defect/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
get_closest_site_id_from_coordinate_and_element,
)
from ....utils import get_center_of_coordinates
from ...utils import transform_coordinate_to_supercell, CoordinateConditionBuilder
from ...utils import transform_coordinate_to_supercell
from ...utils import coordinate as CoordinateCondition
from ..utils import merge_materials
from ..slab import SlabConfiguration, create_slab, Termination
from ..supercell import create_supercell
Expand Down Expand Up @@ -436,7 +437,7 @@ def condition(coordinate: List[float]):
return self.merge_slab_and_defect(island_material, new_material)

def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]:
condition_callable, _ = configuration.condition
condition_callable = configuration.condition.condition
return [
self.create_island(
material=configuration.crystal,
Expand All @@ -463,7 +464,7 @@ def _calculate_cut_direction_vector(self, material: Material, cut_direction: Lis
The normalized cut direction vector in Cartesian coordinates.
"""
np_cut_direction = np.array(cut_direction)
direction_vector = np.dot(np.array(material.basis.cell.vectors_as_nested_array), np_cut_direction)
direction_vector = np.dot(np.array(material.basis.cell.vectors_as_array), np_cut_direction)
normalized_direction_vector = direction_vector / np.linalg.norm(direction_vector)
return normalized_direction_vector

Expand Down Expand Up @@ -499,7 +500,7 @@ def _calculate_rotation_parameters(
"""
height_cartesian = self._calculate_height_cartesian(original_material, new_material)
cut_direction_xy_proj_cart = np.linalg.norm(
np.dot(np.array(new_material.basis.cell.vectors_as_nested_array), normalized_direction_vector)
np.dot(np.array(new_material.basis.cell.vectors_as_array), normalized_direction_vector)
)
# Slope of the terrace along the cut direction
hypotenuse = np.linalg.norm([height_cartesian, cut_direction_xy_proj_cart])
Expand Down Expand Up @@ -592,10 +593,10 @@ def create_terrace(
)

normalized_direction_vector = self._calculate_cut_direction_vector(material, cut_direction)
condition, _ = CoordinateConditionBuilder.plane(
condition = CoordinateCondition.PlaneCoordinateCondition(
plane_normal=normalized_direction_vector,
plane_point_coordinate=pivot_coordinate,
)
).condition
atoms_within_terrace = filter_by_condition_on_coordinates(
material=material_with_additional_layers,
condition=condition,
Expand Down
27 changes: 20 additions & 7 deletions src/py/mat3ra/made/tools/build/defect/configuration.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
from typing import Optional, List, Any, Callable, Dict, Tuple, Union
from typing import Optional, List, Union
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 ...utils import CoordinateConditionBuilder
from ...utils.coordinate import (
CylinderCoordinateCondition,
SphereCoordinateCondition,
BoxCoordinateCondition,
TriangularPrismCoordinateCondition,
PlaneCoordinateCondition,
)
from .enums import PointDefectTypeEnum, SlabDefectTypeEnum, AtomPlacementMethodEnum, ComplexDefectTypeEnum


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

class Config:
arbitrary_types_allowed = True

@property
def _json(self):
Expand Down Expand Up @@ -169,16 +177,21 @@ class IslandSlabDefectConfiguration(SlabDefectConfiguration):
"""

defect_type: SlabDefectTypeEnum = SlabDefectTypeEnum.ISLAND
condition: Optional[Tuple[Callable[[List[float]], bool], Dict]] = CoordinateConditionBuilder().cylinder()
condition: Union[
CylinderCoordinateCondition,
SphereCoordinateCondition,
BoxCoordinateCondition,
TriangularPrismCoordinateCondition,
PlaneCoordinateCondition,
] = CylinderCoordinateCondition()

@property
def _json(self):
_, condition_json = self.condition
return {
**super()._json,
"type": self.get_cls_name(),
"defect_type": self.defect_type.name,
"condition": condition_json,
"condition": self.condition.get_json(),
}


Expand Down
9 changes: 7 additions & 2 deletions src/py/mat3ra/made/tools/build/interface/builders.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Any, List, Optional

import numpy as np
from mat3ra.made.tools.modify import translate_to_z_level
from pydantic import BaseModel
from ase.build.tools import niggli_reduce
from pymatgen.analysis.interfaces.coherent_interfaces import (
Expand Down Expand Up @@ -144,9 +145,13 @@ class ZSLStrainMatchingInterfaceBuilder(ConvertGeneratedItemsPymatgenStructureMi

def _generate(self, configuration: InterfaceConfiguration) -> List[PymatgenInterface]:
generator = ZSLGenerator(**self.build_parameters.strain_matching_parameters.dict())
substrate_with_atoms_translated_to_bottom = translate_to_z_level(
configuration.substrate_configuration.bulk, "bottom"
)
film_with_atoms_translated_to_bottom = translate_to_z_level(configuration.film_configuration.bulk, "bottom")
builder = CoherentInterfaceBuilder(
substrate_structure=to_pymatgen(configuration.substrate_configuration.bulk),
film_structure=to_pymatgen(configuration.film_configuration.bulk),
substrate_structure=to_pymatgen(substrate_with_atoms_translated_to_bottom),
film_structure=to_pymatgen(film_with_atoms_translated_to_bottom),
substrate_miller=configuration.substrate_configuration.miller_indices,
film_miller=configuration.film_configuration.miller_indices,
zslgen=generator,
Expand Down
37 changes: 37 additions & 0 deletions src/py/mat3ra/made/tools/build/perturbation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from typing import Union, Optional

from mat3ra.made.material import Material
from .builders import (
SlabPerturbationBuilder,
DistancePreservingSlabPerturbationBuilder,
CellMatchingDistancePreservingSlabPerturbationBuilder,
)
from .configuration import PerturbationConfiguration


def create_perturbation(
configuration: PerturbationConfiguration,
preserve_distance: Optional[bool] = False,
builder: Union[
SlabPerturbationBuilder,
DistancePreservingSlabPerturbationBuilder,
CellMatchingDistancePreservingSlabPerturbationBuilder,
None,
] = None,
) -> Material:
"""
Return a material with a perturbation applied.
Args:
configuration: The configuration of the perturbation to be applied.
preserve_distance: If True, the builder that preserves the distance between atoms is used.
builder: The builder to be used to create the perturbation.
Returns:
The material with the perturbation applied.
"""
if builder is None:
builder = SlabPerturbationBuilder()
if preserve_distance:
builder = CellMatchingDistancePreservingSlabPerturbationBuilder()
return builder.get_material(configuration)
Loading

0 comments on commit 361e423

Please sign in to comment.