From 4f049c6a7114059159390b74155faba658c6d9f2 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 25 Jul 2024 14:07:20 -0700 Subject: [PATCH 01/57] feat: add first implementations of perturbation --- .../made/tools/build/perturbation/__init__.py | 0 .../made/tools/build/perturbation/builders.py | 32 +++++++++++++++++++ .../tools/build/perturbation/configuration.py | 22 +++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 src/py/mat3ra/made/tools/build/perturbation/__init__.py create mode 100644 src/py/mat3ra/made/tools/build/perturbation/builders.py create mode 100644 src/py/mat3ra/made/tools/build/perturbation/configuration.py diff --git a/src/py/mat3ra/made/tools/build/perturbation/__init__.py b/src/py/mat3ra/made/tools/build/perturbation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/py/mat3ra/made/tools/build/perturbation/builders.py b/src/py/mat3ra/made/tools/build/perturbation/builders.py new file mode 100644 index 00000000..234b39ee --- /dev/null +++ b/src/py/mat3ra/made/tools/build/perturbation/builders.py @@ -0,0 +1,32 @@ +from typing import List, Optional, Any + +from mat3ra.made.material import Material +from mat3ra.made.tools.build import BaseBuilder + +from .configuration import PerturbationConfiguration + + +class PerturbationBuilder(BaseBuilder): + _ConfigurationType: type(PerturbationConfiguration) = PerturbationConfiguration # type: ignore + _GeneratedItemType: Material = Material + + def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]: + """Generate materials with applied perturbations based on the given configuration.""" + new_material = configuration.slab.clone() + new_material.to_cartesian() + new_coordinates = [] + for coord in new_material.basis.coordinates.values: + x, y, z = coord + perturbed_z = z + configuration.amplitude * configuration.perturbation_func(x, y) + new_coordinates.append([x, y, perturbed_z]) + new_basis = new_material.basis.copy() + new_basis.coordinates.values = new_coordinates + new_basis.to_crystal() + new_material.basis = new_basis + return [new_material] + + def _update_material_name(self, material: Material, configuration: _ConfigurationType) -> Material: + """Update material name based on the perturbation details.""" + perturbation_details = f"perturbed_amplitude_{configuration.amplitude}" + material.name = f"{material.name} ({perturbation_details})" + return material diff --git a/src/py/mat3ra/made/tools/build/perturbation/configuration.py b/src/py/mat3ra/made/tools/build/perturbation/configuration.py new file mode 100644 index 00000000..caddb4d9 --- /dev/null +++ b/src/py/mat3ra/made/tools/build/perturbation/configuration.py @@ -0,0 +1,22 @@ +from typing import Callable + +from mat3ra.code.entity import InMemoryEntity +from mat3ra.made.material import Material +from pydantic import BaseModel + + +class PerturbationConfiguration(BaseModel, InMemoryEntity): + slab: Material + perturbation_func: Callable[[float, float], float] + amplitude: float + + class Config: + arbitrary_types_allowed = True + + @property + def _json(self): + return { + "type": "PerturbationConfiguration", + "slab": self.slab.to_json(), + "amplitude": self.amplitude, + } From af0f656e9cc94b0afb1f44f4c7f8003bb6483391 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 25 Jul 2024 19:44:48 -0700 Subject: [PATCH 02/57] update: rename to deformation --- src/py/mat3ra/made/tools/build/perturbation/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/py/mat3ra/made/tools/build/perturbation/__init__.py diff --git a/src/py/mat3ra/made/tools/build/perturbation/__init__.py b/src/py/mat3ra/made/tools/build/perturbation/__init__.py deleted file mode 100644 index e69de29b..00000000 From 15a99d0b192b20ba2d97aee6416c5d9c8e00a5ba Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 25 Jul 2024 19:45:12 -0700 Subject: [PATCH 03/57] update: rename to deformation --- .../made/tools/build/deformation/__init__.py | 0 .../{perturbation => deformation}/builders.py | 18 +++++++------- .../tools/build/deformation/configuration.py | 24 +++++++++++++++++++ .../tools/build/perturbation/configuration.py | 22 ----------------- 4 files changed, 32 insertions(+), 32 deletions(-) create mode 100644 src/py/mat3ra/made/tools/build/deformation/__init__.py rename src/py/mat3ra/made/tools/build/{perturbation => deformation}/builders.py (53%) create mode 100644 src/py/mat3ra/made/tools/build/deformation/configuration.py delete mode 100644 src/py/mat3ra/made/tools/build/perturbation/configuration.py diff --git a/src/py/mat3ra/made/tools/build/deformation/__init__.py b/src/py/mat3ra/made/tools/build/deformation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/py/mat3ra/made/tools/build/perturbation/builders.py b/src/py/mat3ra/made/tools/build/deformation/builders.py similarity index 53% rename from src/py/mat3ra/made/tools/build/perturbation/builders.py rename to src/py/mat3ra/made/tools/build/deformation/builders.py index 234b39ee..f68d680a 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/builders.py +++ b/src/py/mat3ra/made/tools/build/deformation/builders.py @@ -3,22 +3,21 @@ from mat3ra.made.material import Material from mat3ra.made.tools.build import BaseBuilder -from .configuration import PerturbationConfiguration +from .configuration import DeformationConfiguration -class PerturbationBuilder(BaseBuilder): - _ConfigurationType: type(PerturbationConfiguration) = PerturbationConfiguration # type: ignore +class DeformationBuilder(BaseBuilder): + _ConfigurationType: type(DeformationConfiguration) = DeformationConfiguration # type: ignore _GeneratedItemType: Material = Material def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]: - """Generate materials with applied perturbations based on the given configuration.""" + """Generate materials with applied deformation based on the given configuration.""" new_material = configuration.slab.clone() new_material.to_cartesian() new_coordinates = [] for coord in new_material.basis.coordinates.values: - x, y, z = coord - perturbed_z = z + configuration.amplitude * configuration.perturbation_func(x, y) - new_coordinates.append([x, y, perturbed_z]) + perturbed_coord = configuration.deformation_function[0](coord) + new_coordinates.append(perturbed_coord) new_basis = new_material.basis.copy() new_basis.coordinates.values = new_coordinates new_basis.to_crystal() @@ -26,7 +25,6 @@ def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemTyp return [new_material] def _update_material_name(self, material: Material, configuration: _ConfigurationType) -> Material: - """Update material name based on the perturbation details.""" - perturbation_details = f"perturbed_amplitude_{configuration.amplitude}" - material.name = f"{material.name} ({perturbation_details})" + deformation_details = f"Deformation: {configuration.deformation_function[0].__name__}" + material.name = f"{material.name} ({deformation_details})" return material diff --git a/src/py/mat3ra/made/tools/build/deformation/configuration.py b/src/py/mat3ra/made/tools/build/deformation/configuration.py new file mode 100644 index 00000000..00e67a05 --- /dev/null +++ b/src/py/mat3ra/made/tools/build/deformation/configuration.py @@ -0,0 +1,24 @@ +from typing import Callable, List, Dict, Tuple + +from mat3ra.code.entity import InMemoryEntity +from mat3ra.made.material import Material +from pydantic import BaseModel + +from ...utils import DeformationFunctionBuilder + + +class DeformationConfiguration(BaseModel, InMemoryEntity): + slab: Material + deformation_function: Tuple[Callable[[List[float]], float], Dict] = DeformationFunctionBuilder().sine_wave() + + class Config: + arbitrary_types_allowed = True + + @property + def _json(self): + deformation_function_json = self.deformation_function[1] + return { + "type": self.get_cls_name(), + "slab": self.slab.to_json(), + "deformation_function": deformation_function_json, + } diff --git a/src/py/mat3ra/made/tools/build/perturbation/configuration.py b/src/py/mat3ra/made/tools/build/perturbation/configuration.py deleted file mode 100644 index caddb4d9..00000000 --- a/src/py/mat3ra/made/tools/build/perturbation/configuration.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Callable - -from mat3ra.code.entity import InMemoryEntity -from mat3ra.made.material import Material -from pydantic import BaseModel - - -class PerturbationConfiguration(BaseModel, InMemoryEntity): - slab: Material - perturbation_func: Callable[[float, float], float] - amplitude: float - - class Config: - arbitrary_types_allowed = True - - @property - def _json(self): - return { - "type": "PerturbationConfiguration", - "slab": self.slab.to_json(), - "amplitude": self.amplitude, - } From 20b79c39d6657b015cff54faed8ead64bd65bc79 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 25 Jul 2024 19:45:55 -0700 Subject: [PATCH 04/57] feat: add deformation function builder --- src/py/mat3ra/made/tools/utils.py | 49 +++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/py/mat3ra/made/tools/utils.py b/src/py/mat3ra/made/tools/utils.py index a9bdf3f9..20e3b8c6 100644 --- a/src/py/mat3ra/made/tools/utils.py +++ b/src/py/mat3ra/made/tools/utils.py @@ -253,6 +253,37 @@ def transform_coordinate_to_supercell( return converted_array.tolist() +def sine_wave( + coordinate: List[float], amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, axis="x" +) -> List[float]: + """ + Deform a coordinate using a sine wave. + Args: + coordinate (List[float]): The coordinate to deform. + amplitude (float): The amplitude of the sine wave in cartesian coordinates. + wavelength (float): The wavelength of the sine wave in cartesian coordinates. + phase (float): The phase of the sine wave in cartesian coordinates. + axis (str): The axis of the direction of the sine wave. + + Returns: + List[float]: The deformed coordinate. + """ + if axis == "x": + return [ + coordinate[0], + coordinate[1], + coordinate[2] + amplitude * np.sin(2 * np.pi * coordinate[0] / wavelength + phase), + ] + elif axis == "y": + return [ + coordinate[0], + coordinate[1], + coordinate[2] + amplitude * np.sin(2 * np.pi * coordinate[1] / wavelength + phase), + ] + else: + return coordinate + + class CoordinateConditionBuilder: def create_condition(self, condition_type: str, evaluation_func: Callable, **kwargs) -> Tuple[Callable, Dict]: condition_json = {"type": condition_type, **kwargs} @@ -317,3 +348,21 @@ def plane(self, plane_normal: List[float], plane_point_coordinate: List[float]): plane_normal=plane_normal, plane_point_coordinate=plane_point_coordinate, ) + + +class DeformationFunctionBuilder: + def create_deformation_function( + self, deformation_type: str, evaluation_func: Callable, **kwargs + ) -> Tuple[Callable, Dict]: + deformation_json = {"type": deformation_type, **kwargs} + return lambda coordinate: evaluation_func(coordinate, **kwargs), deformation_json + + def sine_wave(self, amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, axis="x"): + return self.create_deformation_function( + deformation_type="sine_wave", + evaluation_func=sine_wave, + amplitude=amplitude, + wavelength=wavelength, + phase=phase, + axis=axis, + ) From 7279daa38e9078aae241fed2542bd0ce97e53ddd Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 25 Jul 2024 20:09:30 -0700 Subject: [PATCH 05/57] update: prepare for continuos deformation --- .../made/tools/build/deformation/builders.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/deformation/builders.py b/src/py/mat3ra/made/tools/build/deformation/builders.py index f68d680a..d10adaf4 100644 --- a/src/py/mat3ra/made/tools/build/deformation/builders.py +++ b/src/py/mat3ra/made/tools/build/deformation/builders.py @@ -10,8 +10,8 @@ class DeformationBuilder(BaseBuilder): _ConfigurationType: type(DeformationConfiguration) = DeformationConfiguration # type: ignore _GeneratedItemType: Material = Material - def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]: - """Generate materials with applied deformation based on the given configuration.""" + @staticmethod + def deform_slab(configuration): new_material = configuration.slab.clone() new_material.to_cartesian() new_coordinates = [] @@ -22,9 +22,19 @@ def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemTyp new_basis.coordinates.values = new_coordinates new_basis.to_crystal() new_material.basis = new_basis + return new_material + + def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]: + """Generate materials with applied deformation based on the given configuration.""" + new_material = self.deform_slab(configuration) return [new_material] def _update_material_name(self, material: Material, configuration: _ConfigurationType) -> Material: deformation_details = f"Deformation: {configuration.deformation_function[0].__name__}" material.name = f"{material.name} ({deformation_details})" return material + + +class ContinuousDeformationBuilder(DeformationBuilder): + pass + From 6939b3961b7c04f7bcca96d5d78ad60e3eff4f46 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 26 Jul 2024 14:30:31 -0700 Subject: [PATCH 06/57] feat: working implementation --- .../made/tools/build/deformation/builders.py | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/deformation/builders.py b/src/py/mat3ra/made/tools/build/deformation/builders.py index d10adaf4..4aa72b0f 100644 --- a/src/py/mat3ra/made/tools/build/deformation/builders.py +++ b/src/py/mat3ra/made/tools/build/deformation/builders.py @@ -1,8 +1,11 @@ from typing import List, Optional, Any +import numpy as np from mat3ra.made.material import Material from mat3ra.made.tools.build import BaseBuilder +from scipy.integrate import quad +from scipy.optimize import root_scalar from .configuration import DeformationConfiguration @@ -13,7 +16,7 @@ class DeformationBuilder(BaseBuilder): @staticmethod def deform_slab(configuration): new_material = configuration.slab.clone() - new_material.to_cartesian() + # new_material.to_cartesian() new_coordinates = [] for coord in new_material.basis.coordinates.values: perturbed_coord = configuration.deformation_function[0](coord) @@ -36,5 +39,49 @@ def _update_material_name(self, material: Material, configuration: _Configuratio class ContinuousDeformationBuilder(DeformationBuilder): - pass + def df_dx(self, x, wavelength, phase): + return (2 * np.pi / wavelength) * np.cos(2 * np.pi * x / wavelength + phase) + + def arc_length_integral(self, x_prime, x, amplitude, wavelength, phase): + def integrand(t): + return np.sqrt(1 + (amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * t / wavelength + phase)) ** 2) + + # Compute the arc length from 0 to x_prime. + calculated_length = quad(func=integrand, a=0, b=x_prime)[0] + return calculated_length - x + + def find_x_prime(self, x, amplitude, wavelength, phase): + # Find x' such that the integral from 0 to x' equals x + result = root_scalar( + self.arc_length_integral, args=(x, amplitude, wavelength, phase), bracket=[0, 10 * x], method="brentq" + ) + return result.root + + def deform_slab_continuously(self, configuration): + new_material = configuration.slab.clone() + new_material.to_cartesian() + new_coordinates = [] + for coord in new_material.basis.coordinates.values: + x_prime = self.find_x_prime( + coord[0], + configuration.deformation_function[1]["amplitude"], + configuration.deformation_function[1]["wavelength"], + configuration.deformation_function[1]["phase"], + ) + print(coord[0], x_prime) + perturbed_coord = configuration.deformation_function[0]([x_prime, coord[1], coord[2]]) + new_coordinates.append(perturbed_coord) + new_basis = new_material.basis.copy() + new_basis.coordinates.values = new_coordinates + new_basis.to_crystal() + new_material.basis = new_basis + + return new_material + + def _generate( + self, configuration: DeformationBuilder._ConfigurationType + ) -> List[DeformationBuilder._GeneratedItemType]: + """Generate materials with applied continuous deformation based on the given configuration.""" + new_material = self.deform_slab_continuously(configuration) + return [new_material] From 95337d6d18aef3c89e14cbfba9270f3f21e85766 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 26 Jul 2024 19:24:17 -0700 Subject: [PATCH 07/57] update: add sine and radial sine --- src/py/mat3ra/made/tools/utils.py | 125 ++++++++++++++++++++++-------- 1 file changed, 94 insertions(+), 31 deletions(-) diff --git a/src/py/mat3ra/made/tools/utils.py b/src/py/mat3ra/made/tools/utils.py index 20e3b8c6..e2648010 100644 --- a/src/py/mat3ra/made/tools/utils.py +++ b/src/py/mat3ra/made/tools/utils.py @@ -253,37 +253,6 @@ def transform_coordinate_to_supercell( return converted_array.tolist() -def sine_wave( - coordinate: List[float], amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, axis="x" -) -> List[float]: - """ - Deform a coordinate using a sine wave. - Args: - coordinate (List[float]): The coordinate to deform. - amplitude (float): The amplitude of the sine wave in cartesian coordinates. - wavelength (float): The wavelength of the sine wave in cartesian coordinates. - phase (float): The phase of the sine wave in cartesian coordinates. - axis (str): The axis of the direction of the sine wave. - - Returns: - List[float]: The deformed coordinate. - """ - if axis == "x": - return [ - coordinate[0], - coordinate[1], - coordinate[2] + amplitude * np.sin(2 * np.pi * coordinate[0] / wavelength + phase), - ] - elif axis == "y": - return [ - coordinate[0], - coordinate[1], - coordinate[2] + amplitude * np.sin(2 * np.pi * coordinate[1] / wavelength + phase), - ] - else: - return coordinate - - class CoordinateConditionBuilder: def create_condition(self, condition_type: str, evaluation_func: Callable, **kwargs) -> Tuple[Callable, Dict]: condition_json = {"type": condition_type, **kwargs} @@ -350,6 +319,91 @@ def plane(self, plane_normal: List[float], plane_point_coordinate: List[float]): ) +def sine_wave( + coordinate: List[float], amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, axis="x" +) -> List[float]: + """ + Deform a coordinate using a sine wave. + Args: + coordinate (List[float]): The coordinate to deform. + amplitude (float): The amplitude of the sine wave in cartesian coordinates. + wavelength (float): The wavelength of the sine wave in cartesian coordinates. + phase (float): The phase of the sine wave in cartesian coordinates. + axis (str): The axis of the direction of the sine wave. + + Returns: + List[float]: The deformed coordinate. + """ + axis_map = {"x": 0, "y": 1} + if axis in axis_map: + index = axis_map[axis] + return [ + coordinate[0], + coordinate[1], + coordinate[2] + amplitude * np.sin(2 * np.pi * coordinate[index] / wavelength + phase), + ] + else: + return coordinate + + +def sine_wave_radial( + coordinate: List[float], amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, center_position=None +) -> List[float]: + """ + Deform a coordinate using a radial sine wave. + Args: + coordinate (List[float]): The coordinate to deform. + amplitude (float): The amplitude of the sine wave in cartesian coordinates. + wavelength (float): The wavelength of the sine wave in cartesian coordinates. + phase (float): The phase of the sine wave in cartesian coordinates. + center_position (List[float]): The center position of the sine wave on the plane. + + Returns: + List[float]: The deformed coordinate. + """ + if center_position is None: + center_position = [0.5, 0.5] + np_position = np.array(coordinate[:2]) + np_center_position = np.array(center_position) + distance = np.linalg.norm(np_position - np_center_position) + return [ + coordinate[0], + coordinate[1], + coordinate[2] + amplitude * np.sin(2 * np.pi * distance / wavelength + phase), + ] + + +def sine_wave_differential( + coordinate: List[float], amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, axis="x" +) -> List[float]: + """ + Deform a coordinate using the differential of a sine wave. + Args: + coordinate (List[float]): The coordinate to deform. + amplitude (float): The amplitude of the sine wave in cartesian coordinates. + wavelength (float): The wavelength of the sine wave in cartesian coordinates. + phase (float): The phase of the sine wave in cartesian coordinates. + axis (str): The axis of the direction of the sine wave. + + Returns: + List[float]: The deformed coordinate. + """ + if axis == "x": + return [ + 0, + 0, + amplitude * np.cos(2 * np.pi * coordinate[0] / wavelength + phase) * 2 * np.pi / wavelength, + ] + elif axis == "y": + return [ + 0, + 0, + amplitude * np.cos(2 * np.pi * coordinate[1] / wavelength + phase) * 2 * np.pi / wavelength, + ] + else: + return [0, 0, 0] + + class DeformationFunctionBuilder: def create_deformation_function( self, deformation_type: str, evaluation_func: Callable, **kwargs @@ -366,3 +420,12 @@ def sine_wave(self, amplitude: float = 0.1, wavelength: float = 1, phase: float phase=phase, axis=axis, ) + + def sine_wave_radial(self, amplitude: float = 0.1, wavelength: float = 1, phase: float = 0): + return self.create_deformation_function( + deformation_type="sine_wave_radial", + evaluation_func=sine_wave_radial, + amplitude=amplitude, + wavelength=wavelength, + phase=phase, + ) From b53ce875585745774c2baf0f7a7ba3eb9348f72a Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 26 Jul 2024 20:24:27 -0700 Subject: [PATCH 08/57] update: add sine helpers --- src/py/mat3ra/made/tools/utils.py | 51 ++++++++++++++----------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/src/py/mat3ra/made/tools/utils.py b/src/py/mat3ra/made/tools/utils.py index e2648010..ed2f23cf 100644 --- a/src/py/mat3ra/made/tools/utils.py +++ b/src/py/mat3ra/made/tools/utils.py @@ -3,6 +3,8 @@ import numpy as np from mat3ra.utils.matrix import convert_2x2_to_3x3 +from scipy.integrate import quad +from scipy.optimize import root_scalar from .third_party import PymatgenStructure @@ -373,35 +375,28 @@ def sine_wave_radial( ] -def sine_wave_differential( - coordinate: List[float], amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, axis="x" -) -> List[float]: - """ - Deform a coordinate using the differential of a sine wave. - Args: - coordinate (List[float]): The coordinate to deform. - amplitude (float): The amplitude of the sine wave in cartesian coordinates. - wavelength (float): The wavelength of the sine wave in cartesian coordinates. - phase (float): The phase of the sine wave in cartesian coordinates. - axis (str): The axis of the direction of the sine wave. +def sine_wave_diff(x, amplitude, wavelength, phase): + return amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * x / wavelength + phase) - Returns: - List[float]: The deformed coordinate. - """ - if axis == "x": - return [ - 0, - 0, - amplitude * np.cos(2 * np.pi * coordinate[0] / wavelength + phase) * 2 * np.pi / wavelength, - ] - elif axis == "y": - return [ - 0, - 0, - amplitude * np.cos(2 * np.pi * coordinate[1] / wavelength + phase) * 2 * np.pi / wavelength, - ] - else: - return [0, 0, 0] + +def sine_wave_length_integral_equation(x_prime, x, amplitude, wavelength, phase): + def integrand(t): + return np.sqrt(1 + (sine_wave_diff(t, amplitude, wavelength, phase)) ** 2) + + arc_length = quad(func=integrand, a=0, b=x_prime)[0] + return arc_length - x + + +def solve_sine_wave_x_prime(self, x, amplitude, wavelength, phase): + # Find x' such that the integral from 0 to x' equals x + COEFFICIENT = 10 + result = root_scalar( + self.arc_length_integral, + args=(x, amplitude, wavelength, phase), + bracket=[0, COEFFICIENT * x], + method="brentq", + ) + return result.root class DeformationFunctionBuilder: From 50658adeee5ffbeff6e14b8ce4cc3ba20ce48d5a Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 26 Jul 2024 20:24:48 -0700 Subject: [PATCH 09/57] update: use sine helpers --- .../made/tools/build/deformation/builders.py | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/deformation/builders.py b/src/py/mat3ra/made/tools/build/deformation/builders.py index 4aa72b0f..c3ea83df 100644 --- a/src/py/mat3ra/made/tools/build/deformation/builders.py +++ b/src/py/mat3ra/made/tools/build/deformation/builders.py @@ -1,12 +1,10 @@ -from typing import List, Optional, Any +from typing import List -import numpy as np from mat3ra.made.material import Material from mat3ra.made.tools.build import BaseBuilder -from scipy.integrate import quad -from scipy.optimize import root_scalar from .configuration import DeformationConfiguration +from ...utils import solve_sine_wave_x_prime class DeformationBuilder(BaseBuilder): @@ -39,36 +37,18 @@ def _update_material_name(self, material: Material, configuration: _Configuratio class ContinuousDeformationBuilder(DeformationBuilder): - def df_dx(self, x, wavelength, phase): - return (2 * np.pi / wavelength) * np.cos(2 * np.pi * x / wavelength + phase) - - def arc_length_integral(self, x_prime, x, amplitude, wavelength, phase): - def integrand(t): - return np.sqrt(1 + (amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * t / wavelength + phase)) ** 2) - - # Compute the arc length from 0 to x_prime. - calculated_length = quad(func=integrand, a=0, b=x_prime)[0] - return calculated_length - x - - def find_x_prime(self, x, amplitude, wavelength, phase): - # Find x' such that the integral from 0 to x' equals x - result = root_scalar( - self.arc_length_integral, args=(x, amplitude, wavelength, phase), bracket=[0, 10 * x], method="brentq" - ) - return result.root def deform_slab_continuously(self, configuration): new_material = configuration.slab.clone() new_material.to_cartesian() new_coordinates = [] for coord in new_material.basis.coordinates.values: - x_prime = self.find_x_prime( + x_prime = solve_sine_wave_x_prime( coord[0], configuration.deformation_function[1]["amplitude"], configuration.deformation_function[1]["wavelength"], configuration.deformation_function[1]["phase"], ) - print(coord[0], x_prime) perturbed_coord = configuration.deformation_function[0]([x_prime, coord[1], coord[2]]) new_coordinates.append(perturbed_coord) From b90b7ab86615153914399a8c0d9aef51700304c4 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 26 Jul 2024 20:25:21 -0700 Subject: [PATCH 10/57] update: helps with second time interface creation --- src/py/mat3ra/made/tools/build/interface/builders.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index 8c2642eb..a26df9c6 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -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 ( @@ -144,9 +145,11 @@ class ZSLStrainMatchingInterfaceBuilder(ConvertGeneratedItemsPymatgenStructureMi def _generate(self, configuration: InterfaceConfiguration) -> List[PymatgenInterface]: generator = ZSLGenerator(**self.build_parameters.strain_matching_parameters.dict()) + substrate_down = translate_to_z_level(configuration.substrate_configuration.bulk, "bottom") + film_down = 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_down), + film_structure=to_pymatgen(film_down), substrate_miller=configuration.substrate_configuration.miller_indices, film_miller=configuration.film_configuration.miller_indices, zslgen=generator, From 11db83308f8d60e9850121cb0344632fd95f6117 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:12:17 -0700 Subject: [PATCH 11/57] update: simply use continuous deformations --- .../made/tools/build/deformation/builders.py | 14 ++-- src/py/mat3ra/made/tools/utils.py | 65 ++++++++++++------- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/deformation/builders.py b/src/py/mat3ra/made/tools/build/deformation/builders.py index c3ea83df..07b860c6 100644 --- a/src/py/mat3ra/made/tools/build/deformation/builders.py +++ b/src/py/mat3ra/made/tools/build/deformation/builders.py @@ -4,7 +4,6 @@ from mat3ra.made.tools.build import BaseBuilder from .configuration import DeformationConfiguration -from ...utils import solve_sine_wave_x_prime class DeformationBuilder(BaseBuilder): @@ -37,19 +36,14 @@ def _update_material_name(self, material: Material, configuration: _Configuratio class ContinuousDeformationBuilder(DeformationBuilder): - def deform_slab_continuously(self, configuration): new_material = configuration.slab.clone() new_material.to_cartesian() new_coordinates = [] + + deformation_function, deformation_json = configuration.deformation_function for coord in new_material.basis.coordinates.values: - x_prime = solve_sine_wave_x_prime( - coord[0], - configuration.deformation_function[1]["amplitude"], - configuration.deformation_function[1]["wavelength"], - configuration.deformation_function[1]["phase"], - ) - perturbed_coord = configuration.deformation_function[0]([x_prime, coord[1], coord[2]]) + perturbed_coord = deformation_function(coord) new_coordinates.append(perturbed_coord) new_basis = new_material.basis.copy() @@ -57,6 +51,8 @@ def deform_slab_continuously(self, configuration): new_basis.to_crystal() new_material.basis = new_basis + # TODO: adjust the lattice parameters with the same coordinates transformation + return new_material def _generate( diff --git a/src/py/mat3ra/made/tools/utils.py b/src/py/mat3ra/made/tools/utils.py index 63206440..9443a76b 100644 --- a/src/py/mat3ra/made/tools/utils.py +++ b/src/py/mat3ra/made/tools/utils.py @@ -353,6 +353,38 @@ def sine_wave( return coordinate +def solve_sine_wave_coordinate_prime( + coordinate: List[float], amplitude: float, wavelength: float, phase: float +) -> List[float]: + def sine_wave_diff(x, amplitude, wavelength, phase): + return amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * x / wavelength + phase) + + def sine_wave_length_integral_equation(x_prime, x, amplitude, wavelength, phase): + def integrand(t): + return np.sqrt(1 + (sine_wave_diff(t, amplitude, wavelength, phase)) ** 2) + + arc_length = quad(func=integrand, a=0, b=x_prime)[0] + return arc_length - x + + COEFFICIENT = 10 + x = coordinate[0] + # Find x' such that the integral from 0 to x' equals x + result = root_scalar( + sine_wave_length_integral_equation, + args=(x, amplitude, wavelength, phase), + bracket=[0, COEFFICIENT * x], + method="brentq", + ) + return [result.root, coordinate[1], coordinate[2]] + + +def sine_wave_continuous( + coordinate: List[float], amplitude: float = 0.1, wavelength: float = 1, phase: float = 0 +) -> List[float]: + coordinate_prime = solve_sine_wave_coordinate_prime(coordinate, amplitude, wavelength, phase) + return sine_wave(coordinate_prime, amplitude, wavelength, phase, axis="x") + + def sine_wave_radial( coordinate: List[float], amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, center_position=None ) -> List[float]: @@ -380,30 +412,6 @@ def sine_wave_radial( ] -def sine_wave_diff(x, amplitude, wavelength, phase): - return amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * x / wavelength + phase) - - -def sine_wave_length_integral_equation(x_prime, x, amplitude, wavelength, phase): - def integrand(t): - return np.sqrt(1 + (sine_wave_diff(t, amplitude, wavelength, phase)) ** 2) - - arc_length = quad(func=integrand, a=0, b=x_prime)[0] - return arc_length - x - - -def solve_sine_wave_x_prime(self, x, amplitude, wavelength, phase): - # Find x' such that the integral from 0 to x' equals x - COEFFICIENT = 10 - result = root_scalar( - self.arc_length_integral, - args=(x, amplitude, wavelength, phase), - bracket=[0, COEFFICIENT * x], - method="brentq", - ) - return result.root - - class DeformationFunctionBuilder: def create_deformation_function( self, deformation_type: str, evaluation_func: Callable, **kwargs @@ -421,6 +429,15 @@ def sine_wave(self, amplitude: float = 0.1, wavelength: float = 1, phase: float axis=axis, ) + def sine_wave_continuous(self, amplitude: float = 0.1, wavelength: float = 1, phase: float = 0): + return self.create_deformation_function( + deformation_type="sine_wave_continuous", + evaluation_func=sine_wave_continuous, + amplitude=amplitude, + wavelength=wavelength, + phase=phase, + ) + def sine_wave_radial(self, amplitude: float = 0.1, wavelength: float = 1, phase: float = 0): return self.create_deformation_function( deformation_type="sine_wave_radial", From b49ca69accf07d706a71c75c6a9fcfd902dc4185 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:20:57 -0700 Subject: [PATCH 12/57] update: optimize --- .../made/tools/build/deformation/builders.py | 2 +- src/py/mat3ra/made/tools/utils.py | 33 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/deformation/builders.py b/src/py/mat3ra/made/tools/build/deformation/builders.py index 07b860c6..b3734e59 100644 --- a/src/py/mat3ra/made/tools/build/deformation/builders.py +++ b/src/py/mat3ra/made/tools/build/deformation/builders.py @@ -13,7 +13,7 @@ class DeformationBuilder(BaseBuilder): @staticmethod def deform_slab(configuration): new_material = configuration.slab.clone() - # new_material.to_cartesian() + new_material.to_cartesian() new_coordinates = [] for coord in new_material.basis.coordinates.values: perturbed_coord = configuration.deformation_function[0](coord) diff --git a/src/py/mat3ra/made/tools/utils.py b/src/py/mat3ra/made/tools/utils.py index 9443a76b..7f4f773d 100644 --- a/src/py/mat3ra/made/tools/utils.py +++ b/src/py/mat3ra/made/tools/utils.py @@ -1,5 +1,5 @@ from functools import wraps -from typing import Callable, Dict, List, Optional, Tuple +from typing import Callable, Dict, List, Optional, Tuple, Literal import numpy as np from mat3ra.utils.matrix import convert_2x2_to_3x3 @@ -10,6 +10,7 @@ DEFAULT_SCALING_FACTOR = np.array([3, 3, 3]) DEFAULT_TRANSLATION_VECTOR = 1 / DEFAULT_SCALING_FACTOR +AXIS_TO_INDEX_MAP = {"x": 0, "y": 1, "z": 2} # TODO: convert to accept ASE Atoms object @@ -327,7 +328,11 @@ def plane(plane_normal: List[float], plane_point_coordinate: List[float]): def sine_wave( - coordinate: List[float], amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, axis="x" + coordinate: List[float], + amplitude: float = 0.1, + wavelength: float = 1, + phase: float = 0, + axis: Literal["x", "y"] = "x", ) -> List[float]: """ Deform a coordinate using a sine wave. @@ -341,9 +346,8 @@ def sine_wave( Returns: List[float]: The deformed coordinate. """ - axis_map = {"x": 0, "y": 1} - if axis in axis_map: - index = axis_map[axis] + if axis in AXIS_TO_INDEX_MAP: + index = AXIS_TO_INDEX_MAP[axis] return [ coordinate[0], coordinate[1], @@ -354,28 +358,31 @@ def sine_wave( def solve_sine_wave_coordinate_prime( - coordinate: List[float], amplitude: float, wavelength: float, phase: float + coordinate: List[float], amplitude: float, wavelength: float, phase: float, axis="x" ) -> List[float]: def sine_wave_diff(x, amplitude, wavelength, phase): return amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * x / wavelength + phase) - def sine_wave_length_integral_equation(x_prime, x, amplitude, wavelength, phase): + def sine_wave_length_integral_equation(w_prime, w, amplitude, wavelength, phase): def integrand(t): return np.sqrt(1 + (sine_wave_diff(t, amplitude, wavelength, phase)) ** 2) - arc_length = quad(func=integrand, a=0, b=x_prime)[0] - return arc_length - x + arc_length = quad(func=integrand, a=0, b=w_prime)[0] + return arc_length - w COEFFICIENT = 10 - x = coordinate[0] + index = AXIS_TO_INDEX_MAP.get(axis, 0) + w = coordinate[index] # Find x' such that the integral from 0 to x' equals x result = root_scalar( sine_wave_length_integral_equation, - args=(x, amplitude, wavelength, phase), - bracket=[0, COEFFICIENT * x], + args=(w, amplitude, wavelength, phase), + bracket=[0, COEFFICIENT * w], method="brentq", ) - return [result.root, coordinate[1], coordinate[2]] + new_coordinate = coordinate[:] + new_coordinate[index] = result.root + return new_coordinate def sine_wave_continuous( From f304dcbae4da09b311f4eb7259960b9725f55231 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 30 Jul 2024 19:37:23 -0700 Subject: [PATCH 13/57] update: types, cleanup --- .../made/tools/build/deformation/builders.py | 4 +- src/py/mat3ra/made/tools/utils.py | 47 +++++++++++-------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/deformation/builders.py b/src/py/mat3ra/made/tools/build/deformation/builders.py index b3734e59..2ea22b15 100644 --- a/src/py/mat3ra/made/tools/build/deformation/builders.py +++ b/src/py/mat3ra/made/tools/build/deformation/builders.py @@ -36,7 +36,7 @@ def _update_material_name(self, material: Material, configuration: _Configuratio class ContinuousDeformationBuilder(DeformationBuilder): - def deform_slab_continuously(self, configuration): + def deform_slab_isometrically(self, configuration): new_material = configuration.slab.clone() new_material.to_cartesian() new_coordinates = [] @@ -59,5 +59,5 @@ def _generate( self, configuration: DeformationBuilder._ConfigurationType ) -> List[DeformationBuilder._GeneratedItemType]: """Generate materials with applied continuous deformation based on the given configuration.""" - new_material = self.deform_slab_continuously(configuration) + new_material = self.deform_slab_isometrically(configuration) return [new_material] diff --git a/src/py/mat3ra/made/tools/utils.py b/src/py/mat3ra/made/tools/utils.py index 7f4f773d..a92113f0 100644 --- a/src/py/mat3ra/made/tools/utils.py +++ b/src/py/mat3ra/made/tools/utils.py @@ -1,5 +1,5 @@ from functools import wraps -from typing import Callable, Dict, List, Optional, Tuple, Literal +from typing import Callable, Dict, List, Literal, Optional, Tuple import numpy as np from mat3ra.utils.matrix import convert_2x2_to_3x3 @@ -11,6 +11,7 @@ DEFAULT_SCALING_FACTOR = np.array([3, 3, 3]) DEFAULT_TRANSLATION_VECTOR = 1 / DEFAULT_SCALING_FACTOR AXIS_TO_INDEX_MAP = {"x": 0, "y": 1, "z": 2} +EQUATION_RANGE_COEFFICIENT = 10 # TODO: convert to accept ASE Atoms object @@ -360,24 +361,22 @@ def sine_wave( def solve_sine_wave_coordinate_prime( coordinate: List[float], amplitude: float, wavelength: float, phase: float, axis="x" ) -> List[float]: - def sine_wave_diff(x, amplitude, wavelength, phase): - return amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * x / wavelength + phase) + def sine_wave_diff(w: float, amplitude: float, wavelength: float, phase: float) -> float: + return amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * w / wavelength + phase) - def sine_wave_length_integral_equation(w_prime, w, amplitude, wavelength, phase): - def integrand(t): - return np.sqrt(1 + (sine_wave_diff(t, amplitude, wavelength, phase)) ** 2) - - arc_length = quad(func=integrand, a=0, b=w_prime)[0] + def sine_wave_length_integral_equation(w_prime: float, w: float, amplitude: float, wavelength: float, phase: float): + arc_length = quad( + lambda t: np.sqrt(1 + (sine_wave_diff(t, amplitude, wavelength, phase)) ** 2), a=0, b=w_prime + )[0] return arc_length - w - COEFFICIENT = 10 index = AXIS_TO_INDEX_MAP.get(axis, 0) w = coordinate[index] # Find x' such that the integral from 0 to x' equals x result = root_scalar( sine_wave_length_integral_equation, args=(w, amplitude, wavelength, phase), - bracket=[0, COEFFICIENT * w], + bracket=[0, EQUATION_RANGE_COEFFICIENT * w], method="brentq", ) new_coordinate = coordinate[:] @@ -385,10 +384,14 @@ def integrand(t): return new_coordinate -def sine_wave_continuous( - coordinate: List[float], amplitude: float = 0.1, wavelength: float = 1, phase: float = 0 +def sine_wave_isometric( + coordinate: List[float], + amplitude: float = 0.1, + wavelength: float = 1, + phase: float = 0, + axis: Literal["x", "y"] = "x", ) -> List[float]: - coordinate_prime = solve_sine_wave_coordinate_prime(coordinate, amplitude, wavelength, phase) + coordinate_prime = solve_sine_wave_coordinate_prime(coordinate, amplitude, wavelength, phase, axis) return sine_wave(coordinate_prime, amplitude, wavelength, phase, axis="x") @@ -426,9 +429,9 @@ def create_deformation_function( deformation_json = {"type": deformation_type, **kwargs} return lambda coordinate: evaluation_func(coordinate, **kwargs), deformation_json - def sine_wave(self, amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, axis="x"): + def sine_wave(self, amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, axis: Literal["x", "y"] = "x"): return self.create_deformation_function( - deformation_type="sine_wave", + deformation_type=sine_wave.__name__, evaluation_func=sine_wave, amplitude=amplitude, wavelength=wavelength, @@ -436,20 +439,24 @@ def sine_wave(self, amplitude: float = 0.1, wavelength: float = 1, phase: float axis=axis, ) - def sine_wave_continuous(self, amplitude: float = 0.1, wavelength: float = 1, phase: float = 0): + def sine_wave_isometric( + self, amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, axis: Literal["x", "y"] = "x" + ): return self.create_deformation_function( - deformation_type="sine_wave_continuous", - evaluation_func=sine_wave_continuous, + deformation_type=sine_wave_isometric.__name__, + evaluation_func=sine_wave_isometric, amplitude=amplitude, wavelength=wavelength, phase=phase, + axis=axis, ) - def sine_wave_radial(self, amplitude: float = 0.1, wavelength: float = 1, phase: float = 0): + def sine_wave_radial(self, amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, center_position=None): return self.create_deformation_function( - deformation_type="sine_wave_radial", + deformation_type=sine_wave_radial.__name__, evaluation_func=sine_wave_radial, amplitude=amplitude, wavelength=wavelength, phase=phase, + center_position=center_position, ) From 9e98b26e4b8ef4fe9053557ac377a87c9cef93b7 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:19:06 -0700 Subject: [PATCH 14/57] update: fix and wrap --- .../made/tools/build/deformation/builders.py | 3 ++- src/py/mat3ra/made/tools/modify.py | 14 ++++++++++++++ src/py/mat3ra/made/tools/utils.py | 6 +++--- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/deformation/builders.py b/src/py/mat3ra/made/tools/build/deformation/builders.py index 2ea22b15..1351df40 100644 --- a/src/py/mat3ra/made/tools/build/deformation/builders.py +++ b/src/py/mat3ra/made/tools/build/deformation/builders.py @@ -4,6 +4,7 @@ from mat3ra.made.tools.build import BaseBuilder from .configuration import DeformationConfiguration +from ...modify import wrap_material class DeformationBuilder(BaseBuilder): @@ -51,8 +52,8 @@ def deform_slab_isometrically(self, configuration): new_basis.to_crystal() new_material.basis = new_basis + new_material = wrap_material(new_material) # TODO: adjust the lattice parameters with the same coordinates transformation - return new_material def _generate( diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index 983640bd..0d3d86f4 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -437,3 +437,17 @@ def rotate_material(material: Material, axis: List[int], angle: float) -> Materi atoms.wrap() return Material(from_ase(atoms)) + + +def wrap_material(material: Material) -> Material: + """ + Wrap the material to the unit cell. + + Args: + material (Material): The material to wrap. + Returns: + Material: The wrapped material. + """ + atoms = to_ase(material) + atoms.wrap() + return Material(from_ase(atoms)) diff --git a/src/py/mat3ra/made/tools/utils.py b/src/py/mat3ra/made/tools/utils.py index a92113f0..9c3a1ce9 100644 --- a/src/py/mat3ra/made/tools/utils.py +++ b/src/py/mat3ra/made/tools/utils.py @@ -359,7 +359,7 @@ def sine_wave( def solve_sine_wave_coordinate_prime( - coordinate: List[float], amplitude: float, wavelength: float, phase: float, axis="x" + coordinate: List[float], amplitude: float, wavelength: float, phase: float, axis: Literal["x", "y"] ) -> List[float]: def sine_wave_diff(w: float, amplitude: float, wavelength: float, phase: float) -> float: return amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * w / wavelength + phase) @@ -370,7 +370,7 @@ def sine_wave_length_integral_equation(w_prime: float, w: float, amplitude: floa )[0] return arc_length - w - index = AXIS_TO_INDEX_MAP.get(axis, 0) + index = AXIS_TO_INDEX_MAP[axis] w = coordinate[index] # Find x' such that the integral from 0 to x' equals x result = root_scalar( @@ -392,7 +392,7 @@ def sine_wave_isometric( axis: Literal["x", "y"] = "x", ) -> List[float]: coordinate_prime = solve_sine_wave_coordinate_prime(coordinate, amplitude, wavelength, phase, axis) - return sine_wave(coordinate_prime, amplitude, wavelength, phase, axis="x") + return sine_wave(coordinate_prime, amplitude, wavelength, phase, axis=axis) def sine_wave_radial( From 425892d979548c6fc7b4863cfbc3f8d4e4890fee Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:49:48 -0700 Subject: [PATCH 15/57] update: move functions to holder --- .../tools/build/deformation/configuration.py | 4 +- src/py/mat3ra/made/tools/utils.py | 258 +++++++++--------- 2 files changed, 135 insertions(+), 127 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/deformation/configuration.py b/src/py/mat3ra/made/tools/build/deformation/configuration.py index 00e67a05..2313f9d7 100644 --- a/src/py/mat3ra/made/tools/build/deformation/configuration.py +++ b/src/py/mat3ra/made/tools/build/deformation/configuration.py @@ -4,12 +4,12 @@ from mat3ra.made.material import Material from pydantic import BaseModel -from ...utils import DeformationFunctionBuilder +from ...utils import DeformationFunctionHolder class DeformationConfiguration(BaseModel, InMemoryEntity): slab: Material - deformation_function: Tuple[Callable[[List[float]], float], Dict] = DeformationFunctionBuilder().sine_wave() + deformation_function: Tuple[Callable[[List[float]], float], Dict] = DeformationFunctionHolder.sine_wave() class Config: arbitrary_types_allowed = True diff --git a/src/py/mat3ra/made/tools/utils.py b/src/py/mat3ra/made/tools/utils.py index 9c3a1ce9..830ec599 100644 --- a/src/py/mat3ra/made/tools/utils.py +++ b/src/py/mat3ra/made/tools/utils.py @@ -328,135 +328,143 @@ def plane(plane_normal: List[float], plane_point_coordinate: List[float]): ) -def sine_wave( - coordinate: List[float], - amplitude: float = 0.1, - wavelength: float = 1, - phase: float = 0, - axis: Literal["x", "y"] = "x", -) -> List[float]: - """ - Deform a coordinate using a sine wave. - Args: - coordinate (List[float]): The coordinate to deform. - amplitude (float): The amplitude of the sine wave in cartesian coordinates. - wavelength (float): The wavelength of the sine wave in cartesian coordinates. - phase (float): The phase of the sine wave in cartesian coordinates. - axis (str): The axis of the direction of the sine wave. - - Returns: - List[float]: The deformed coordinate. - """ - if axis in AXIS_TO_INDEX_MAP: - index = AXIS_TO_INDEX_MAP[axis] - return [ - coordinate[0], - coordinate[1], - coordinate[2] + amplitude * np.sin(2 * np.pi * coordinate[index] / wavelength + phase), - ] - else: - return coordinate - - -def solve_sine_wave_coordinate_prime( - coordinate: List[float], amplitude: float, wavelength: float, phase: float, axis: Literal["x", "y"] -) -> List[float]: - def sine_wave_diff(w: float, amplitude: float, wavelength: float, phase: float) -> float: - return amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * w / wavelength + phase) - - def sine_wave_length_integral_equation(w_prime: float, w: float, amplitude: float, wavelength: float, phase: float): - arc_length = quad( - lambda t: np.sqrt(1 + (sine_wave_diff(t, amplitude, wavelength, phase)) ** 2), a=0, b=w_prime - )[0] - return arc_length - w - - index = AXIS_TO_INDEX_MAP[axis] - w = coordinate[index] - # Find x' such that the integral from 0 to x' equals x - result = root_scalar( - sine_wave_length_integral_equation, - args=(w, amplitude, wavelength, phase), - bracket=[0, EQUATION_RANGE_COEFFICIENT * w], - method="brentq", - ) - new_coordinate = coordinate[:] - new_coordinate[index] = result.root - return new_coordinate - - -def sine_wave_isometric( - coordinate: List[float], - amplitude: float = 0.1, - wavelength: float = 1, - phase: float = 0, - axis: Literal["x", "y"] = "x", -) -> List[float]: - coordinate_prime = solve_sine_wave_coordinate_prime(coordinate, amplitude, wavelength, phase, axis) - return sine_wave(coordinate_prime, amplitude, wavelength, phase, axis=axis) +class DeformationFunctionHolder: + @staticmethod + def sine_wave( + amplitude: float = 0.1, + wavelength: float = 1, + phase: float = 0, + axis: Literal["x", "y"] = "x", + ) -> Tuple[Callable[[List[float]], List[float]], Dict]: + """ + Deform a coordinate using a sine wave. + Args: + amplitude (float): The amplitude of the sine wave in cartesian coordinates. + wavelength (float): The wavelength of the sine wave in cartesian coordinates. + phase (float): The phase of the sine wave in cartesian coordinates. + axis (str): The axis of the direction of the sine wave. + + Returns: + Tuple[Callable[[List[float]], List[float]], Dict]: The deformation function and its configuration + """ + if axis in AXIS_TO_INDEX_MAP: + index = AXIS_TO_INDEX_MAP[axis] + + def deformation(coordinate: List[float]): + return [ + coordinate[0], + coordinate[1], + coordinate[2] + amplitude * np.sin(2 * np.pi * coordinate[index] / wavelength + phase), + ] + + config = {"type": "sine_wave", "amplitude": amplitude, "wavelength": wavelength, "phase": phase, "axis": axis} + + return deformation, config -def sine_wave_radial( - coordinate: List[float], amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, center_position=None -) -> List[float]: - """ - Deform a coordinate using a radial sine wave. - Args: - coordinate (List[float]): The coordinate to deform. - amplitude (float): The amplitude of the sine wave in cartesian coordinates. - wavelength (float): The wavelength of the sine wave in cartesian coordinates. - phase (float): The phase of the sine wave in cartesian coordinates. - center_position (List[float]): The center position of the sine wave on the plane. + @staticmethod + def _solve_sine_wave_coordinate_prime( + coordinate: List[float], amplitude: float, wavelength: float, phase: float, axis: Literal["x", "y"] + ) -> List[float]: + def sine_wave_diff(w: float, amplitude: float, wavelength: float, phase: float) -> float: + return amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * w / wavelength + phase) + + def sine_wave_length_integral_equation( + w_prime: float, w: float, amplitude: float, wavelength: float, phase: float + ): + arc_length = quad( + lambda t: np.sqrt(1 + (sine_wave_diff(t, amplitude, wavelength, phase)) ** 2), a=0, b=w_prime + )[0] + return arc_length - w - Returns: - List[float]: The deformed coordinate. - """ - if center_position is None: - center_position = [0.5, 0.5] - np_position = np.array(coordinate[:2]) - np_center_position = np.array(center_position) - distance = np.linalg.norm(np_position - np_center_position) - return [ - coordinate[0], - coordinate[1], - coordinate[2] + amplitude * np.sin(2 * np.pi * distance / wavelength + phase), - ] - - -class DeformationFunctionBuilder: - def create_deformation_function( - self, deformation_type: str, evaluation_func: Callable, **kwargs - ) -> Tuple[Callable, Dict]: - deformation_json = {"type": deformation_type, **kwargs} - return lambda coordinate: evaluation_func(coordinate, **kwargs), deformation_json - - def sine_wave(self, amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, axis: Literal["x", "y"] = "x"): - return self.create_deformation_function( - deformation_type=sine_wave.__name__, - evaluation_func=sine_wave, - amplitude=amplitude, - wavelength=wavelength, - phase=phase, - axis=axis, + index = AXIS_TO_INDEX_MAP[axis] + w = coordinate[index] + # Find x' such that the integral from 0 to x' equals x + result = root_scalar( + sine_wave_length_integral_equation, + args=(w, amplitude, wavelength, phase), + bracket=[0, EQUATION_RANGE_COEFFICIENT * w], + method="brentq", ) + new_coordinate = coordinate[:] + new_coordinate[index] = result.root + return new_coordinate + @staticmethod def sine_wave_isometric( - self, amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, axis: Literal["x", "y"] = "x" - ): - return self.create_deformation_function( - deformation_type=sine_wave_isometric.__name__, - evaluation_func=sine_wave_isometric, - amplitude=amplitude, - wavelength=wavelength, - phase=phase, - axis=axis, - ) + amplitude: float = 0.1, + wavelength: float = 1, + phase: float = 0, + axis: Literal["x", "y"] = "x", + ) -> Tuple[Callable[[List[float]], List[float]], Dict]: + """ + Deform a coordinate using a sine wave isometrically. + + Args: + amplitude (float): The amplitude of the sine wave in cartesian coordinates. + wavelength (float): The wavelength of the sine wave in cartesian coordinates. + phase (float): The phase of the sine wave in cartesian coordinates. + axis (str): The axis of the direction of the sine wave. + + Returns: + Tuple[Callable[[List[float]], List[float]], Dict]: The deformation function and its configuration + """ + + def sine_wave_isometric(coordinate: List[float]): + new_coordinate = DeformationFunctionHolder._solve_sine_wave_coordinate_prime( + coordinate, amplitude, wavelength, phase, axis + ) + return [ + new_coordinate[0], + new_coordinate[1], + coordinate[2] + + amplitude * np.sin(2 * np.pi * new_coordinate[AXIS_TO_INDEX_MAP[axis]] / wavelength + phase), + ] + + config = { + "type": "sine_wave_isometric", + "amplitude": amplitude, + "wavelength": wavelength, + "phase": phase, + "axis": axis, + } + + return sine_wave_isometric, config - def sine_wave_radial(self, amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, center_position=None): - return self.create_deformation_function( - deformation_type=sine_wave_radial.__name__, - evaluation_func=sine_wave_radial, - amplitude=amplitude, - wavelength=wavelength, - phase=phase, - center_position=center_position, - ) + @staticmethod + def sine_wave_radial( + amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, center_position=None + ) -> Tuple[Callable[[List[float]], List[float]], Dict]: + """ + Deform a coordinate using a radial sine wave. + Args: + amplitude (float): The amplitude of the sine wave in cartesian coordinates. + wavelength (float): The wavelength of the sine wave in cartesian coordinates. + phase (float): The phase of the sine wave in cartesian coordinates. + center_position (List[float]): The center position of the sine wave on the plane. + + Returns: + Tuple[Callable[[List[float]], List[float]], Dict]: The deformation function and its configuration + """ + if center_position is None: + center_position = [0.5, 0.5] + + def radial_sine_wave(coordinate: List[float]): + np_position = np.array(coordinate[:2]) + np_center_position = np.array(center_position) + distance = np.linalg.norm(np_position - np_center_position) + return [ + coordinate[0], + coordinate[1], + coordinate[2] + amplitude * np.sin(2 * np.pi * distance / wavelength + phase), + ] + + config = { + "type": "sine_wave_radial", + "amplitude": amplitude, + "wavelength": wavelength, + "phase": phase, + "center_position": center_position, + } + + return radial_sine_wave, config From c7f8d5383597d5e4a19dea7b7a1f6b8740aac622 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:58:58 -0700 Subject: [PATCH 16/57] update: OOP inheritance --- .../made/tools/build/deformation/builders.py | 16 +++++++++++----- .../tools/build/deformation/configuration.py | 6 ++++-- src/py/mat3ra/made/tools/utils.py | 1 - 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/deformation/builders.py b/src/py/mat3ra/made/tools/build/deformation/builders.py index 1351df40..0d64de0f 100644 --- a/src/py/mat3ra/made/tools/build/deformation/builders.py +++ b/src/py/mat3ra/made/tools/build/deformation/builders.py @@ -11,6 +11,8 @@ class DeformationBuilder(BaseBuilder): _ConfigurationType: type(DeformationConfiguration) = DeformationConfiguration # type: ignore _GeneratedItemType: Material = Material + +class SlabDeformationBuilder(DeformationBuilder): @staticmethod def deform_slab(configuration): new_material = configuration.slab.clone() @@ -25,21 +27,26 @@ def deform_slab(configuration): new_material.basis = new_basis return new_material - def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]: + def _generate( + self, configuration: DeformationBuilder._ConfigurationType + ) -> List[DeformationBuilder._GeneratedItemType]: """Generate materials with applied deformation based on the given configuration.""" new_material = self.deform_slab(configuration) return [new_material] - def _update_material_name(self, material: Material, configuration: _ConfigurationType) -> Material: + def _update_material_name( + self, material: Material, configuration: DeformationBuilder._ConfigurationType + ) -> Material: deformation_details = f"Deformation: {configuration.deformation_function[0].__name__}" material.name = f"{material.name} ({deformation_details})" return material -class ContinuousDeformationBuilder(DeformationBuilder): +class DistancePreservingSlabDeformationBuilder(DeformationBuilder): def deform_slab_isometrically(self, configuration): new_material = configuration.slab.clone() - new_material.to_cartesian() + if configuration.use_cartesian_coordinates: + new_material.to_cartesian() new_coordinates = [] deformation_function, deformation_json = configuration.deformation_function @@ -53,7 +60,6 @@ def deform_slab_isometrically(self, configuration): new_material.basis = new_basis new_material = wrap_material(new_material) - # TODO: adjust the lattice parameters with the same coordinates transformation return new_material def _generate( diff --git a/src/py/mat3ra/made/tools/build/deformation/configuration.py b/src/py/mat3ra/made/tools/build/deformation/configuration.py index 2313f9d7..f50f0e12 100644 --- a/src/py/mat3ra/made/tools/build/deformation/configuration.py +++ b/src/py/mat3ra/made/tools/build/deformation/configuration.py @@ -1,4 +1,4 @@ -from typing import Callable, List, Dict, Tuple +from typing import Callable, Dict, Tuple from mat3ra.code.entity import InMemoryEntity from mat3ra.made.material import Material @@ -9,7 +9,8 @@ class DeformationConfiguration(BaseModel, InMemoryEntity): slab: Material - deformation_function: Tuple[Callable[[List[float]], float], Dict] = DeformationFunctionHolder.sine_wave() + deformation_function: Tuple[Callable, Dict] = DeformationFunctionHolder.sine_wave() + use_cartesian_coordinates: bool = True class Config: arbitrary_types_allowed = True @@ -21,4 +22,5 @@ def _json(self): "type": self.get_cls_name(), "slab": self.slab.to_json(), "deformation_function": deformation_function_json, + "use_cartesian_coordinates": self.use_cartesian_coordinates, } diff --git a/src/py/mat3ra/made/tools/utils.py b/src/py/mat3ra/made/tools/utils.py index 830ec599..65e34d02 100644 --- a/src/py/mat3ra/made/tools/utils.py +++ b/src/py/mat3ra/made/tools/utils.py @@ -329,7 +329,6 @@ def plane(plane_normal: List[float], plane_point_coordinate: List[float]): class DeformationFunctionHolder: - @staticmethod def sine_wave( amplitude: float = 0.1, From 3861d28300af3f28bac870e5d12d70aed204db60 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 31 Jul 2024 21:00:34 -0700 Subject: [PATCH 17/57] update: rename slab -> material --- src/py/mat3ra/made/tools/build/deformation/builders.py | 4 ++-- src/py/mat3ra/made/tools/build/deformation/configuration.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/deformation/builders.py b/src/py/mat3ra/made/tools/build/deformation/builders.py index 0d64de0f..15dc0cc5 100644 --- a/src/py/mat3ra/made/tools/build/deformation/builders.py +++ b/src/py/mat3ra/made/tools/build/deformation/builders.py @@ -15,7 +15,7 @@ class DeformationBuilder(BaseBuilder): class SlabDeformationBuilder(DeformationBuilder): @staticmethod def deform_slab(configuration): - new_material = configuration.slab.clone() + new_material = configuration.material.clone() new_material.to_cartesian() new_coordinates = [] for coord in new_material.basis.coordinates.values: @@ -44,7 +44,7 @@ def _update_material_name( class DistancePreservingSlabDeformationBuilder(DeformationBuilder): def deform_slab_isometrically(self, configuration): - new_material = configuration.slab.clone() + new_material = configuration.material.clone() if configuration.use_cartesian_coordinates: new_material.to_cartesian() new_coordinates = [] diff --git a/src/py/mat3ra/made/tools/build/deformation/configuration.py b/src/py/mat3ra/made/tools/build/deformation/configuration.py index f50f0e12..11c4fdc9 100644 --- a/src/py/mat3ra/made/tools/build/deformation/configuration.py +++ b/src/py/mat3ra/made/tools/build/deformation/configuration.py @@ -8,7 +8,7 @@ class DeformationConfiguration(BaseModel, InMemoryEntity): - slab: Material + material: Material deformation_function: Tuple[Callable, Dict] = DeformationFunctionHolder.sine_wave() use_cartesian_coordinates: bool = True @@ -20,7 +20,7 @@ def _json(self): deformation_function_json = self.deformation_function[1] return { "type": self.get_cls_name(), - "slab": self.slab.to_json(), + "material": self.material.to_json(), "deformation_function": deformation_function_json, "use_cartesian_coordinates": self.use_cartesian_coordinates, } From 83325e3c7c16359cab2bc0555f9df072bac1bb01 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 31 Jul 2024 21:04:01 -0700 Subject: [PATCH 18/57] update: rename deformation -> perturbation --- .../{deformation => perturbation}/__init__.py | 0 .../{deformation => perturbation}/builders.py | 34 +++++++++---------- .../configuration.py | 10 +++--- src/py/mat3ra/made/tools/utils.py | 10 +++--- 4 files changed, 27 insertions(+), 27 deletions(-) rename src/py/mat3ra/made/tools/build/{deformation => perturbation}/__init__.py (100%) rename src/py/mat3ra/made/tools/build/{deformation => perturbation}/builders.py (56%) rename src/py/mat3ra/made/tools/build/{deformation => perturbation}/configuration.py (61%) diff --git a/src/py/mat3ra/made/tools/build/deformation/__init__.py b/src/py/mat3ra/made/tools/build/perturbation/__init__.py similarity index 100% rename from src/py/mat3ra/made/tools/build/deformation/__init__.py rename to src/py/mat3ra/made/tools/build/perturbation/__init__.py diff --git a/src/py/mat3ra/made/tools/build/deformation/builders.py b/src/py/mat3ra/made/tools/build/perturbation/builders.py similarity index 56% rename from src/py/mat3ra/made/tools/build/deformation/builders.py rename to src/py/mat3ra/made/tools/build/perturbation/builders.py index 15dc0cc5..9a47af14 100644 --- a/src/py/mat3ra/made/tools/build/deformation/builders.py +++ b/src/py/mat3ra/made/tools/build/perturbation/builders.py @@ -3,23 +3,23 @@ from mat3ra.made.material import Material from mat3ra.made.tools.build import BaseBuilder -from .configuration import DeformationConfiguration +from .configuration import PerturbationConfiguration from ...modify import wrap_material -class DeformationBuilder(BaseBuilder): - _ConfigurationType: type(DeformationConfiguration) = DeformationConfiguration # type: ignore +class PerturbationBuilder(BaseBuilder): + _ConfigurationType: type(PerturbationConfiguration) = PerturbationConfiguration # type: ignore _GeneratedItemType: Material = Material -class SlabDeformationBuilder(DeformationBuilder): +class SlabPerturbationBuilder(PerturbationBuilder): @staticmethod def deform_slab(configuration): new_material = configuration.material.clone() new_material.to_cartesian() new_coordinates = [] for coord in new_material.basis.coordinates.values: - perturbed_coord = configuration.deformation_function[0](coord) + perturbed_coord = configuration.perturbation_function[0](coord) new_coordinates.append(perturbed_coord) new_basis = new_material.basis.copy() new_basis.coordinates.values = new_coordinates @@ -28,30 +28,30 @@ def deform_slab(configuration): return new_material def _generate( - self, configuration: DeformationBuilder._ConfigurationType - ) -> List[DeformationBuilder._GeneratedItemType]: - """Generate materials with applied deformation based on the given configuration.""" + self, configuration: PerturbationBuilder._ConfigurationType + ) -> List[PerturbationBuilder._GeneratedItemType]: + """Generate materials with applied perturbation based on the given configuration.""" new_material = self.deform_slab(configuration) return [new_material] def _update_material_name( - self, material: Material, configuration: DeformationBuilder._ConfigurationType + self, material: Material, configuration: PerturbationBuilder._ConfigurationType ) -> Material: - deformation_details = f"Deformation: {configuration.deformation_function[0].__name__}" - material.name = f"{material.name} ({deformation_details})" + perturbation_details = f"Perturbation: {configuration.perturbation_function[0].__name__}" + material.name = f"{material.name} ({perturbation_details})" return material -class DistancePreservingSlabDeformationBuilder(DeformationBuilder): +class DistancePreservingSlabPerturbationBuilder(PerturbationBuilder): def deform_slab_isometrically(self, configuration): new_material = configuration.material.clone() if configuration.use_cartesian_coordinates: new_material.to_cartesian() new_coordinates = [] - deformation_function, deformation_json = configuration.deformation_function + perturbation_function, perturbation_json = configuration.perturbation_function for coord in new_material.basis.coordinates.values: - perturbed_coord = deformation_function(coord) + perturbed_coord = perturbation_function(coord) new_coordinates.append(perturbed_coord) new_basis = new_material.basis.copy() @@ -63,8 +63,8 @@ def deform_slab_isometrically(self, configuration): return new_material def _generate( - self, configuration: DeformationBuilder._ConfigurationType - ) -> List[DeformationBuilder._GeneratedItemType]: - """Generate materials with applied continuous deformation based on the given configuration.""" + self, configuration: PerturbationBuilder._ConfigurationType + ) -> List[PerturbationBuilder._GeneratedItemType]: + """Generate materials with applied continuous perturbation based on the given configuration.""" new_material = self.deform_slab_isometrically(configuration) return [new_material] diff --git a/src/py/mat3ra/made/tools/build/deformation/configuration.py b/src/py/mat3ra/made/tools/build/perturbation/configuration.py similarity index 61% rename from src/py/mat3ra/made/tools/build/deformation/configuration.py rename to src/py/mat3ra/made/tools/build/perturbation/configuration.py index 11c4fdc9..c5ab2018 100644 --- a/src/py/mat3ra/made/tools/build/deformation/configuration.py +++ b/src/py/mat3ra/made/tools/build/perturbation/configuration.py @@ -4,12 +4,12 @@ from mat3ra.made.material import Material from pydantic import BaseModel -from ...utils import DeformationFunctionHolder +from ...utils import PerturbationFunctionHolder -class DeformationConfiguration(BaseModel, InMemoryEntity): +class PerturbationConfiguration(BaseModel, InMemoryEntity): material: Material - deformation_function: Tuple[Callable, Dict] = DeformationFunctionHolder.sine_wave() + perturbation_function: Tuple[Callable, Dict] = PerturbationFunctionHolder.sine_wave() use_cartesian_coordinates: bool = True class Config: @@ -17,10 +17,10 @@ class Config: @property def _json(self): - deformation_function_json = self.deformation_function[1] + perturbation_function_json = self.perturbation_function[1] return { "type": self.get_cls_name(), "material": self.material.to_json(), - "deformation_function": deformation_function_json, + "perturbation_function": perturbation_function_json, "use_cartesian_coordinates": self.use_cartesian_coordinates, } diff --git a/src/py/mat3ra/made/tools/utils.py b/src/py/mat3ra/made/tools/utils.py index 65e34d02..9ff01a7a 100644 --- a/src/py/mat3ra/made/tools/utils.py +++ b/src/py/mat3ra/made/tools/utils.py @@ -328,7 +328,7 @@ def plane(plane_normal: List[float], plane_point_coordinate: List[float]): ) -class DeformationFunctionHolder: +class PerturbationFunctionHolder: @staticmethod def sine_wave( amplitude: float = 0.1, @@ -345,7 +345,7 @@ def sine_wave( axis (str): The axis of the direction of the sine wave. Returns: - Tuple[Callable[[List[float]], List[float]], Dict]: The deformation function and its configuration + Tuple[Callable[[List[float]], List[float]], Dict]: The perturbation function and its configuration """ if axis in AXIS_TO_INDEX_MAP: index = AXIS_TO_INDEX_MAP[axis] @@ -406,11 +406,11 @@ def sine_wave_isometric( axis (str): The axis of the direction of the sine wave. Returns: - Tuple[Callable[[List[float]], List[float]], Dict]: The deformation function and its configuration + Tuple[Callable[[List[float]], List[float]], Dict]: The perturbation function and its configuration """ def sine_wave_isometric(coordinate: List[float]): - new_coordinate = DeformationFunctionHolder._solve_sine_wave_coordinate_prime( + new_coordinate = PerturbationFunctionHolder._solve_sine_wave_coordinate_prime( coordinate, amplitude, wavelength, phase, axis ) return [ @@ -443,7 +443,7 @@ def sine_wave_radial( center_position (List[float]): The center position of the sine wave on the plane. Returns: - Tuple[Callable[[List[float]], List[float]], Dict]: The deformation function and its configuration + Tuple[Callable[[List[float]], List[float]], Dict]: The perturbation function and its configuration """ if center_position is None: center_position = [0.5, 0.5] From e512d29471c273436772c201f453e4ed72b1f544 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 1 Aug 2024 11:49:31 -0700 Subject: [PATCH 19/57] update: make work with distinct builders --- .../made/tools/build/perturbation/builders.py | 8 +- .../tools/build/perturbation/configuration.py | 2 +- src/py/mat3ra/made/tools/utils.py | 116 ++++++++++-------- 3 files changed, 70 insertions(+), 56 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/perturbation/builders.py b/src/py/mat3ra/made/tools/build/perturbation/builders.py index 9a47af14..6c7179fa 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/builders.py +++ b/src/py/mat3ra/made/tools/build/perturbation/builders.py @@ -5,6 +5,7 @@ from .configuration import PerturbationConfiguration from ...modify import wrap_material +from ...utils import PerturbationFunctionHolder class PerturbationBuilder(BaseBuilder): @@ -18,8 +19,9 @@ def deform_slab(configuration): new_material = configuration.material.clone() new_material.to_cartesian() new_coordinates = [] + perturbation_function, _ = configuration.perturbation_function for coord in new_material.basis.coordinates.values: - perturbed_coord = configuration.perturbation_function[0](coord) + perturbed_coord = perturbation_function(coord) new_coordinates.append(perturbed_coord) new_basis = new_material.basis.copy() new_basis.coordinates.values = new_coordinates @@ -50,8 +52,10 @@ def deform_slab_isometrically(self, configuration): new_coordinates = [] perturbation_function, perturbation_json = configuration.perturbation_function + coord_transformation_function = PerturbationFunctionHolder.get_coord_transformation(perturbation_json) for coord in new_material.basis.coordinates.values: - perturbed_coord = perturbation_function(coord) + transformed_coord = coord_transformation_function(coord) + perturbed_coord = perturbation_function(transformed_coord) new_coordinates.append(perturbed_coord) new_basis = new_material.basis.copy() diff --git a/src/py/mat3ra/made/tools/build/perturbation/configuration.py b/src/py/mat3ra/made/tools/build/perturbation/configuration.py index c5ab2018..bf89bfa5 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/configuration.py +++ b/src/py/mat3ra/made/tools/build/perturbation/configuration.py @@ -17,7 +17,7 @@ class Config: @property def _json(self): - perturbation_function_json = self.perturbation_function[1] + _, perturbation_function_json = self.perturbation_function return { "type": self.get_cls_name(), "material": self.material.to_json(), diff --git a/src/py/mat3ra/made/tools/utils.py b/src/py/mat3ra/made/tools/utils.py index 9ff01a7a..1748621e 100644 --- a/src/py/mat3ra/made/tools/utils.py +++ b/src/py/mat3ra/made/tools/utils.py @@ -329,12 +329,18 @@ def plane(plane_normal: List[float], plane_point_coordinate: List[float]): class PerturbationFunctionHolder: + @staticmethod + def get_coord_transformation(perturbation_json: dict) -> Callable: + name = perturbation_json.pop("type") + map = {"sine_wave": PerturbationFunctionHolder._solve_sine_wave_coordinate_prime} + return map[name](**perturbation_json) + @staticmethod def sine_wave( amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, - axis: Literal["x", "y"] = "x", + axis: Optional[Literal["x", "y"]] = "x", ) -> Tuple[Callable[[List[float]], List[float]], Dict]: """ Deform a coordinate using a sine wave. @@ -363,8 +369,8 @@ def deformation(coordinate: List[float]): @staticmethod def _solve_sine_wave_coordinate_prime( - coordinate: List[float], amplitude: float, wavelength: float, phase: float, axis: Literal["x", "y"] - ) -> List[float]: + amplitude: float, wavelength: float, phase: float, axis: Literal["x", "y"] + ) -> Callable[[List[float]], List[float]]: def sine_wave_diff(w: float, amplitude: float, wavelength: float, phase: float) -> float: return amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * w / wavelength + phase) @@ -377,58 +383,62 @@ def sine_wave_length_integral_equation( return arc_length - w index = AXIS_TO_INDEX_MAP[axis] - w = coordinate[index] - # Find x' such that the integral from 0 to x' equals x - result = root_scalar( - sine_wave_length_integral_equation, - args=(w, amplitude, wavelength, phase), - bracket=[0, EQUATION_RANGE_COEFFICIENT * w], - method="brentq", - ) - new_coordinate = coordinate[:] - new_coordinate[index] = result.root - return new_coordinate - - @staticmethod - def sine_wave_isometric( - amplitude: float = 0.1, - wavelength: float = 1, - phase: float = 0, - axis: Literal["x", "y"] = "x", - ) -> Tuple[Callable[[List[float]], List[float]], Dict]: - """ - Deform a coordinate using a sine wave isometrically. - - Args: - amplitude (float): The amplitude of the sine wave in cartesian coordinates. - wavelength (float): The wavelength of the sine wave in cartesian coordinates. - phase (float): The phase of the sine wave in cartesian coordinates. - axis (str): The axis of the direction of the sine wave. - - Returns: - Tuple[Callable[[List[float]], List[float]], Dict]: The perturbation function and its configuration - """ - def sine_wave_isometric(coordinate: List[float]): - new_coordinate = PerturbationFunctionHolder._solve_sine_wave_coordinate_prime( - coordinate, amplitude, wavelength, phase, axis + def deformation(coordinate: List[float]): + w = coordinate[index] + # Find x' such that the integral from 0 to x' equals x + result = root_scalar( + sine_wave_length_integral_equation, + args=(w, amplitude, wavelength, phase), + bracket=[0, EQUATION_RANGE_COEFFICIENT * w], + method="brentq", ) - return [ - new_coordinate[0], - new_coordinate[1], - coordinate[2] - + amplitude * np.sin(2 * np.pi * new_coordinate[AXIS_TO_INDEX_MAP[axis]] / wavelength + phase), - ] - - config = { - "type": "sine_wave_isometric", - "amplitude": amplitude, - "wavelength": wavelength, - "phase": phase, - "axis": axis, - } - - return sine_wave_isometric, config + new_coordinate = coordinate[:] + new_coordinate[index] = result.root + return new_coordinate + + return deformation + + # @staticmethod + # def sine_wave_isometric( + # amplitude: float = 0.1, + # wavelength: float = 1, + # phase: float = 0, + # axis: Literal["x", "y"] = "x", + # ) -> Tuple[Callable[[List[float]], List[float]], Dict]: + # """ + # Deform a coordinate using a sine wave isometrically. + # + # Args: + # amplitude (float): The amplitude of the sine wave in cartesian coordinates. + # wavelength (float): The wavelength of the sine wave in cartesian coordinates. + # phase (float): The phase of the sine wave in cartesian coordinates. + # axis (str): The axis of the direction of the sine wave. + # + # Returns: + # Tuple[Callable[[List[float]], List[float]], Dict]: The perturbation function and its configuration + # """ + # + # def sine_wave_isometric(coordinate: List[float]): + # new_coordinate = PerturbationFunctionHolder._solve_sine_wave_coordinate_prime( + # coordinate, amplitude, wavelength, phase, axis + # ) + # return [ + # new_coordinate[0], + # new_coordinate[1], + # coordinate[2] + # + amplitude * np.sin(2 * np.pi * new_coordinate[AXIS_TO_INDEX_MAP[axis]] / wavelength + phase), + # ] + # + # config = { + # "type": "sine_wave_isometric", + # "amplitude": amplitude, + # "wavelength": wavelength, + # "phase": phase, + # "axis": axis, + # } + # + # return sine_wave_isometric, config @staticmethod def sine_wave_radial( From 678eda1b5e91135bf6ff5b322a5b6853c657e1e9 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 1 Aug 2024 12:19:47 -0700 Subject: [PATCH 20/57] update: use OOP and cleanup --- .../made/tools/build/perturbation/builders.py | 63 ++++++++----------- src/py/mat3ra/made/tools/utils.py | 52 ++------------- 2 files changed, 31 insertions(+), 84 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/perturbation/builders.py b/src/py/mat3ra/made/tools/build/perturbation/builders.py index 6c7179fa..18589835 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/builders.py +++ b/src/py/mat3ra/made/tools/build/perturbation/builders.py @@ -12,63 +12,52 @@ class PerturbationBuilder(BaseBuilder): _ConfigurationType: type(PerturbationConfiguration) = PerturbationConfiguration # type: ignore _GeneratedItemType: Material = Material - -class SlabPerturbationBuilder(PerturbationBuilder): - @staticmethod - def deform_slab(configuration): + def _prepare_material(self, configuration): new_material = configuration.material.clone() - new_material.to_cartesian() - new_coordinates = [] - perturbation_function, _ = configuration.perturbation_function - for coord in new_material.basis.coordinates.values: - perturbed_coord = perturbation_function(coord) - new_coordinates.append(perturbed_coord) + if configuration.use_cartesian_coordinates: + new_material.to_cartesian() + return new_material + + def _adjust_material(self, new_material, new_coordinates): new_basis = new_material.basis.copy() new_basis.coordinates.values = new_coordinates new_basis.to_crystal() new_material.basis = new_basis return new_material - def _generate( - self, configuration: PerturbationBuilder._ConfigurationType - ) -> List[PerturbationBuilder._GeneratedItemType]: - """Generate materials with applied perturbation based on the given configuration.""" - new_material = self.deform_slab(configuration) + def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]: + """Generate materials with applied continuous perturbation based on the given configuration.""" + new_material = self.create_perturbed_slab(configuration) return [new_material] - def _update_material_name( - self, material: Material, configuration: PerturbationBuilder._ConfigurationType - ) -> Material: - perturbation_details = f"Perturbation: {configuration.perturbation_function[0].__name__}" + def _update_material_name(self, material: Material, configuration: _ConfigurationType) -> Material: + perturbation_details = f"Perturbation: {configuration.perturbation_function[1].get('type')}" material.name = f"{material.name} ({perturbation_details})" return material -class DistancePreservingSlabPerturbationBuilder(PerturbationBuilder): - def deform_slab_isometrically(self, configuration): - new_material = configuration.material.clone() - if configuration.use_cartesian_coordinates: - new_material.to_cartesian() +class SlabPerturbationBuilder(PerturbationBuilder): + def create_perturbed_slab(self, configuration): + new_material = self._prepare_material(configuration) new_coordinates = [] + perturbation_function, _ = configuration.perturbation_function + for coord in new_material.basis.coordinates.values: + perturbed_coord = perturbation_function(coord) + new_coordinates.append(perturbed_coord) + new_material = self._adjust_material(new_material, new_coordinates) + return new_material + +class DistancePreservingSlabPerturbationBuilder(PerturbationBuilder): + def create_perturbed_slab(self, configuration): + new_material = self._prepare_material(configuration) + new_coordinates = [] perturbation_function, perturbation_json = configuration.perturbation_function coord_transformation_function = PerturbationFunctionHolder.get_coord_transformation(perturbation_json) for coord in new_material.basis.coordinates.values: transformed_coord = coord_transformation_function(coord) perturbed_coord = perturbation_function(transformed_coord) new_coordinates.append(perturbed_coord) - - new_basis = new_material.basis.copy() - new_basis.coordinates.values = new_coordinates - new_basis.to_crystal() - new_material.basis = new_basis - + new_material = self._adjust_material(new_material, new_coordinates) new_material = wrap_material(new_material) return new_material - - def _generate( - self, configuration: PerturbationBuilder._ConfigurationType - ) -> List[PerturbationBuilder._GeneratedItemType]: - """Generate materials with applied continuous perturbation based on the given configuration.""" - new_material = self.deform_slab_isometrically(configuration) - return [new_material] diff --git a/src/py/mat3ra/made/tools/utils.py b/src/py/mat3ra/made/tools/utils.py index 1748621e..2fc3cd28 100644 --- a/src/py/mat3ra/made/tools/utils.py +++ b/src/py/mat3ra/made/tools/utils.py @@ -332,8 +332,8 @@ class PerturbationFunctionHolder: @staticmethod def get_coord_transformation(perturbation_json: dict) -> Callable: name = perturbation_json.pop("type") - map = {"sine_wave": PerturbationFunctionHolder._solve_sine_wave_coordinate_prime} - return map[name](**perturbation_json) + name_to_function_map = {"sine_wave": PerturbationFunctionHolder.sine_wave_transform_coordinates} + return name_to_function_map[name](**perturbation_json) @staticmethod def sine_wave( @@ -368,7 +368,7 @@ def deformation(coordinate: List[float]): return deformation, config @staticmethod - def _solve_sine_wave_coordinate_prime( + def sine_wave_transform_coordinates( amplitude: float, wavelength: float, phase: float, axis: Literal["x", "y"] ) -> Callable[[List[float]], List[float]]: def sine_wave_diff(w: float, amplitude: float, wavelength: float, phase: float) -> float: @@ -393,53 +393,11 @@ def deformation(coordinate: List[float]): bracket=[0, EQUATION_RANGE_COEFFICIENT * w], method="brentq", ) - new_coordinate = coordinate[:] - new_coordinate[index] = result.root - return new_coordinate + coordinate[index] = result.root + return coordinate return deformation - # @staticmethod - # def sine_wave_isometric( - # amplitude: float = 0.1, - # wavelength: float = 1, - # phase: float = 0, - # axis: Literal["x", "y"] = "x", - # ) -> Tuple[Callable[[List[float]], List[float]], Dict]: - # """ - # Deform a coordinate using a sine wave isometrically. - # - # Args: - # amplitude (float): The amplitude of the sine wave in cartesian coordinates. - # wavelength (float): The wavelength of the sine wave in cartesian coordinates. - # phase (float): The phase of the sine wave in cartesian coordinates. - # axis (str): The axis of the direction of the sine wave. - # - # Returns: - # Tuple[Callable[[List[float]], List[float]], Dict]: The perturbation function and its configuration - # """ - # - # def sine_wave_isometric(coordinate: List[float]): - # new_coordinate = PerturbationFunctionHolder._solve_sine_wave_coordinate_prime( - # coordinate, amplitude, wavelength, phase, axis - # ) - # return [ - # new_coordinate[0], - # new_coordinate[1], - # coordinate[2] - # + amplitude * np.sin(2 * np.pi * new_coordinate[AXIS_TO_INDEX_MAP[axis]] / wavelength + phase), - # ] - # - # config = { - # "type": "sine_wave_isometric", - # "amplitude": amplitude, - # "wavelength": wavelength, - # "phase": phase, - # "axis": axis, - # } - # - # return sine_wave_isometric, config - @staticmethod def sine_wave_radial( amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, center_position=None From a879f52f7e2df4ade6abdb0042ec36b24bdf66fd Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 1 Aug 2024 12:22:18 -0700 Subject: [PATCH 21/57] update: small adjustments --- .../mat3ra/made/tools/build/perturbation/builders.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/perturbation/builders.py b/src/py/mat3ra/made/tools/build/perturbation/builders.py index 18589835..2cc36be1 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/builders.py +++ b/src/py/mat3ra/made/tools/build/perturbation/builders.py @@ -12,13 +12,15 @@ class PerturbationBuilder(BaseBuilder): _ConfigurationType: type(PerturbationConfiguration) = PerturbationConfiguration # type: ignore _GeneratedItemType: Material = Material - def _prepare_material(self, configuration): + @staticmethod + def _prepare_material(configuration): new_material = configuration.material.clone() if configuration.use_cartesian_coordinates: new_material.to_cartesian() return new_material - def _adjust_material(self, new_material, new_coordinates): + @staticmethod + def _set_new_coordinates(new_material, new_coordinates): new_basis = new_material.basis.copy() new_basis.coordinates.values = new_coordinates new_basis.to_crystal() @@ -44,7 +46,7 @@ def create_perturbed_slab(self, configuration): for coord in new_material.basis.coordinates.values: perturbed_coord = perturbation_function(coord) new_coordinates.append(perturbed_coord) - new_material = self._adjust_material(new_material, new_coordinates) + new_material = self._set_new_coordinates(new_material, new_coordinates) return new_material @@ -58,6 +60,6 @@ def create_perturbed_slab(self, configuration): transformed_coord = coord_transformation_function(coord) perturbed_coord = perturbation_function(transformed_coord) new_coordinates.append(perturbed_coord) - new_material = self._adjust_material(new_material, new_coordinates) + new_material = self._set_new_coordinates(new_material, new_coordinates) new_material = wrap_material(new_material) return new_material From 7cb6053dea1f10c17e80b8c921363dffeab83ac9 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 1 Aug 2024 12:36:51 -0700 Subject: [PATCH 22/57] chore: add docstring --- src/py/mat3ra/made/tools/utils.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/utils.py b/src/py/mat3ra/made/tools/utils.py index 2fc3cd28..332acfb3 100644 --- a/src/py/mat3ra/made/tools/utils.py +++ b/src/py/mat3ra/made/tools/utils.py @@ -371,6 +371,20 @@ def deformation(coordinate: List[float]): def sine_wave_transform_coordinates( amplitude: float, wavelength: float, phase: float, axis: Literal["x", "y"] ) -> Callable[[List[float]], List[float]]: + """ + Transform coordinates to preserve the distance between points on a sine wave. + Achieved by calculating the integral of the length between [0,0,0] and given coordinate. + + Args: + amplitude (float): The amplitude of the sine wave in cartesian coordinates. + wavelength (float): The wavelength of the sine wave in cartesian coordinates. + phase (float): The phase of the sine wave in cartesian coordinates. + axis (str): The axis of the direction of the sine wave. + + Returns: + Callable[[List[float]], List[float]]: The coordinates transformation function. + """ + def sine_wave_diff(w: float, amplitude: float, wavelength: float, phase: float) -> float: return amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * w / wavelength + phase) @@ -384,7 +398,7 @@ def sine_wave_length_integral_equation( index = AXIS_TO_INDEX_MAP[axis] - def deformation(coordinate: List[float]): + def coordinate_transformation(coordinate: List[float]): w = coordinate[index] # Find x' such that the integral from 0 to x' equals x result = root_scalar( @@ -396,7 +410,7 @@ def deformation(coordinate: List[float]): coordinate[index] = result.root return coordinate - return deformation + return coordinate_transformation @staticmethod def sine_wave_radial( From 07cf54c66a9d32dcf4309b42c8afc41dd0d3a40a Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 1 Aug 2024 13:12:39 -0700 Subject: [PATCH 23/57] update: even more optimization --- .../made/tools/build/perturbation/builders.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/perturbation/builders.py b/src/py/mat3ra/made/tools/build/perturbation/builders.py index 2cc36be1..04d7dfc9 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/builders.py +++ b/src/py/mat3ra/made/tools/build/perturbation/builders.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from mat3ra.made.material import Material from mat3ra.made.tools.build import BaseBuilder @@ -11,6 +11,7 @@ class PerturbationBuilder(BaseBuilder): _ConfigurationType: type(PerturbationConfiguration) = PerturbationConfiguration # type: ignore _GeneratedItemType: Material = Material + _PostProcessParametersType = None @staticmethod def _prepare_material(configuration): @@ -32,6 +33,11 @@ def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemTyp new_material = self.create_perturbed_slab(configuration) return [new_material] + def _post_process( + self, items: List[_GeneratedItemType], post_process_parameters: Optional[_PostProcessParametersType] + ) -> List[Material]: + return [wrap_material(item) for item in items] + def _update_material_name(self, material: Material, configuration: _ConfigurationType) -> Material: perturbation_details = f"Perturbation: {configuration.perturbation_function[1].get('type')}" material.name = f"{material.name} ({perturbation_details})" @@ -41,25 +47,20 @@ def _update_material_name(self, material: Material, configuration: _Configuratio class SlabPerturbationBuilder(PerturbationBuilder): def create_perturbed_slab(self, configuration): new_material = self._prepare_material(configuration) - new_coordinates = [] perturbation_function, _ = configuration.perturbation_function - for coord in new_material.basis.coordinates.values: - perturbed_coord = perturbation_function(coord) - new_coordinates.append(perturbed_coord) + new_coordinates = [perturbation_function(coord) for coord in new_material.basis.coordinates.values] new_material = self._set_new_coordinates(new_material, new_coordinates) return new_material -class DistancePreservingSlabPerturbationBuilder(PerturbationBuilder): +class DistancePreservingSlabPerturbationBuilder(SlabPerturbationBuilder): def create_perturbed_slab(self, configuration): new_material = self._prepare_material(configuration) - new_coordinates = [] perturbation_function, perturbation_json = configuration.perturbation_function coord_transformation_function = PerturbationFunctionHolder.get_coord_transformation(perturbation_json) - for coord in new_material.basis.coordinates.values: - transformed_coord = coord_transformation_function(coord) - perturbed_coord = perturbation_function(transformed_coord) - new_coordinates.append(perturbed_coord) + new_coordinates = [ + perturbation_function(coord_transformation_function(coord)) + for coord in new_material.basis.coordinates.values + ] new_material = self._set_new_coordinates(new_material, new_coordinates) - new_material = wrap_material(new_material) return new_material From 2b0805ab15b26b495b501e5ece5eb0b254df40ca Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:02:26 -0700 Subject: [PATCH 24/57] update: fix a mistake --- src/py/mat3ra/made/tools/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/utils.py b/src/py/mat3ra/made/tools/utils.py index 332acfb3..2bce1596 100644 --- a/src/py/mat3ra/made/tools/utils.py +++ b/src/py/mat3ra/made/tools/utils.py @@ -331,9 +331,10 @@ def plane(plane_normal: List[float], plane_point_coordinate: List[float]): class PerturbationFunctionHolder: @staticmethod def get_coord_transformation(perturbation_json: dict) -> Callable: - name = perturbation_json.pop("type") + new_perturbation_json = perturbation_json.copy() + name = new_perturbation_json.pop("type") name_to_function_map = {"sine_wave": PerturbationFunctionHolder.sine_wave_transform_coordinates} - return name_to_function_map[name](**perturbation_json) + return name_to_function_map[name](**new_perturbation_json) @staticmethod def sine_wave( From 7d410a78a856ec7d484683cb28f5183b5f16aef6 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:03:11 -0700 Subject: [PATCH 25/57] feat: add cell matching dpspb --- .../made/tools/build/perturbation/builders.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/py/mat3ra/made/tools/build/perturbation/builders.py b/src/py/mat3ra/made/tools/build/perturbation/builders.py index 04d7dfc9..9c32b9ca 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/builders.py +++ b/src/py/mat3ra/made/tools/build/perturbation/builders.py @@ -64,3 +64,21 @@ def create_perturbed_slab(self, configuration): ] new_material = self._set_new_coordinates(new_material, new_coordinates) return new_material + + +class CellMatchingDistancePreservingSlabPerturbationBuilder(DistancePreservingSlabPerturbationBuilder): + def _transform_cell_vectors(self, configuration: PerturbationConfiguration) -> List[List[float]]: + perturbation_function, perturbation_json = configuration.perturbation_function + coord_transformation_function = PerturbationFunctionHolder.get_coord_transformation(perturbation_json) + cell_vectors = configuration.material.basis.cell.vectors_as_nested_array + return [perturbation_function(coord_transformation_function(coord)) for coord in cell_vectors] + + def create_perturbed_slab(self, configuration: PerturbationConfiguration): + new_material = super().create_perturbed_slab(configuration) + new_lattice_vectors = self._transform_cell_vectors(configuration) + new_basis = new_material.basis.copy() + new_basis.to_cartesian() + new_basis.cell = new_basis.cell.from_nested_array(new_lattice_vectors) + new_basis.to_crystal() + new_material.basis = new_basis + return new_material From 2ecd3cf0d15562545d1a4fe3144cd90e39d3707b Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 1 Aug 2024 20:44:31 -0700 Subject: [PATCH 26/57] feat: add method to create lattice --- src/py/mat3ra/made/lattice.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/py/mat3ra/made/lattice.py b/src/py/mat3ra/made/lattice.py index 3cf7ff57..e6e4f442 100644 --- a/src/py/mat3ra/made/lattice.py +++ b/src/py/mat3ra/made/lattice.py @@ -52,6 +52,17 @@ def vectors(self) -> List[List[float]]: [0.0, 0.0, c], ] + def from_nested_array(self, vectors: List[List[float]]) -> "Lattice": + 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))) + return Lattice( + a=float(a), b=float(b), c=float(c), alpha=alpha, beta=beta, gamma=gamma, units=self.units, type=self.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 From 34eec9ebf1c1bc9c9e2eda7370c993beaa13a645 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 1 Aug 2024 20:49:13 -0700 Subject: [PATCH 27/57] update: adjustments: --- src/py/mat3ra/made/tools/build/perturbation/builders.py | 4 ++++ src/py/mat3ra/made/tools/utils.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/py/mat3ra/made/tools/build/perturbation/builders.py b/src/py/mat3ra/made/tools/build/perturbation/builders.py index 9c32b9ca..825199ca 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/builders.py +++ b/src/py/mat3ra/made/tools/build/perturbation/builders.py @@ -76,6 +76,10 @@ def _transform_cell_vectors(self, configuration: PerturbationConfiguration) -> L def create_perturbed_slab(self, configuration: PerturbationConfiguration): new_material = super().create_perturbed_slab(configuration) new_lattice_vectors = self._transform_cell_vectors(configuration) + new_lattice = new_material.lattice.copy() + new_lattice = new_lattice.from_nested_array(new_lattice_vectors) + new_material.lattice = new_lattice + new_basis = new_material.basis.copy() new_basis.to_cartesian() new_basis.cell = new_basis.cell.from_nested_array(new_lattice_vectors) diff --git a/src/py/mat3ra/made/tools/utils.py b/src/py/mat3ra/made/tools/utils.py index 2bce1596..4deaf4ff 100644 --- a/src/py/mat3ra/made/tools/utils.py +++ b/src/py/mat3ra/made/tools/utils.py @@ -11,7 +11,7 @@ DEFAULT_SCALING_FACTOR = np.array([3, 3, 3]) DEFAULT_TRANSLATION_VECTOR = 1 / DEFAULT_SCALING_FACTOR AXIS_TO_INDEX_MAP = {"x": 0, "y": 1, "z": 2} -EQUATION_RANGE_COEFFICIENT = 10 +EQUATION_RANGE_COEFFICIENT = 5 # TODO: convert to accept ASE Atoms object From e8f403a49bc0e8be0d2659c182330027615898ae Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:05:21 -0700 Subject: [PATCH 28/57] update: add a test for perturbations --- tests/py/unit/fixtures.py | 31 ++++++++++++++++++- .../py/unit/test_tools_build_perturbation.py | 23 ++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 tests/py/unit/test_tools_build_perturbation.py diff --git a/tests/py/unit/fixtures.py b/tests/py/unit/fixtures.py index 384d21a1..193a9af5 100644 --- a/tests/py/unit/fixtures.py +++ b/tests/py/unit/fixtures.py @@ -114,7 +114,7 @@ } SI_SUPERCELL_2X2X1: Dict[str, Any] = { - "name": "Si8", + "name": "Silicon FCC", "basis": { "elements": [ {"id": 0, "value": "Si"}, @@ -260,3 +260,32 @@ ) t_001 = get_terminations(slab_001_config)[0] SLAB_001 = create_slab(slab_001_config, t_001) + +GRAPHENE = { + "name": "Graphene", + "basis": { + "elements": [{"id": 0, "value": "C"}, {"id": 1, "value": "C"}], + "coordinates": [{"id": 0, "value": [0, 0, 0]}, {"id": 1, "value": [0.333333, 0.666667, 0]}], + "units": "crystal", + "cell": [[2.467291, 0, 0], [-1.2336454999, 2.1367366845, 0], [0, 0, 20]], + "constraints": [], + }, + "lattice": { + "a": 2.467291, + "b": 2.467291, + "c": 20, + "alpha": 90, + "beta": 90, + "gamma": 120, + "units": {"length": "angstrom", "angle": "degree"}, + "type": "HEX", + "vectors": { + "a": [2.467291, 0, 0], + "b": [-1.233645, 2.136737, 0], + "c": [0, 0, 20], + "alat": 1, + "units": "angstrom", + }, + }, + "isNonPeriodic": False, +} diff --git a/tests/py/unit/test_tools_build_perturbation.py b/tests/py/unit/test_tools_build_perturbation.py new file mode 100644 index 00000000..347b7e4b --- /dev/null +++ b/tests/py/unit/test_tools_build_perturbation.py @@ -0,0 +1,23 @@ +from mat3ra.made.material import Material +from mat3ra.made.tools.build.perturbation.builders import SlabPerturbationBuilder +from mat3ra.made.tools.build.perturbation.configuration import PerturbationConfiguration +from mat3ra.made.tools.build.supercell import create_supercell +from mat3ra.made.tools.utils import PerturbationFunctionHolder +from mat3ra.utils import assertion as assertion_utils +from .fixtures import GRAPHENE + + +def test_sine_perturbation(): + material = Material(GRAPHENE) + slab = create_supercell(material, [[10, 0, 0], [0, 10, 0], [0, 0, 1]]) + + perturbation_config = PerturbationConfiguration( + material=slab, + perturbation_function=PerturbationFunctionHolder.sine_wave(amplitude=0.05, wavelength=1), + use_cartesian_coordinates=False, + ) + builder = SlabPerturbationBuilder() + perturbed_slab = builder.get_material(perturbation_config) + # Check selected atoms to avoid using 100+ atoms fixture + assertion_utils.assert_deep_almost_equal([0.0, 0.0, 0.5], perturbed_slab.basis.coordinates.values[0]) + assertion_utils.assert_deep_almost_equal([0.2, 0.1, 0.547552826], perturbed_slab.basis.coordinates.values[42]) From 4d44046e59eb4a61891abcac7b9718878215bd15 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:09:20 -0700 Subject: [PATCH 29/57] update: small but nessessary change --- src/py/mat3ra/made/tools/build/perturbation/builders.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/py/mat3ra/made/tools/build/perturbation/builders.py b/src/py/mat3ra/made/tools/build/perturbation/builders.py index 825199ca..8bdd6d19 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/builders.py +++ b/src/py/mat3ra/made/tools/build/perturbation/builders.py @@ -4,7 +4,7 @@ from mat3ra.made.tools.build import BaseBuilder from .configuration import PerturbationConfiguration -from ...modify import wrap_material +from ...modify import wrap_material, translate_to_z_level from ...utils import PerturbationFunctionHolder @@ -16,6 +16,7 @@ class PerturbationBuilder(BaseBuilder): @staticmethod def _prepare_material(configuration): new_material = configuration.material.clone() + new_material = translate_to_z_level(new_material, "center") if configuration.use_cartesian_coordinates: new_material.to_cartesian() return new_material From 830140a5a981ece358f31d396afdab3cd33a51ad Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:17:40 -0700 Subject: [PATCH 30/57] update: noticed and issue with name and metadata --- src/py/mat3ra/made/tools/build/supercell.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/supercell.py b/src/py/mat3ra/made/tools/build/supercell.py index 52de72a5..1a58df84 100644 --- a/src/py/mat3ra/made/tools/build/supercell.py +++ b/src/py/mat3ra/made/tools/build/supercell.py @@ -4,26 +4,30 @@ from mat3ra.made.material import Material from ..third_party import ASEAtoms, ase_make_supercell from ..utils import decorator_convert_2x2_to_3x3 -from ..convert import from_ase, decorator_convert_material_args_kwargs_to_atoms +from ..convert import from_ase, decorator_convert_material_args_kwargs_to_atoms, to_ase @decorator_convert_2x2_to_3x3 -@decorator_convert_material_args_kwargs_to_atoms def create_supercell( - atoms: ASEAtoms, supercell_matrix: Optional[List[List[int]]] = None, scaling_factor: Optional[List[int]] = None + material: Material, supercell_matrix: Optional[List[List[int]]] = None, scaling_factor: Optional[List[int]] = None ) -> Material: """ Create a supercell of the atoms. Args: - atoms (Material): The atoms to create a supercell of. + material (Material): The atoms to create a supercell of. supercell_matrix (List[List[int]]): The supercell matrix (e.g. [[3,0,0],[0,3,0],[0,0,1]]). scaling_factor (List[int], optional): The scaling factor instead of matrix (e.g. [3,3,1]). Returns: Material: The supercell of the atoms. """ + atoms = to_ase(material) if scaling_factor is not None: supercell_matrix = np.multiply(scaling_factor, np.eye(3)).tolist() supercell_atoms = ase_make_supercell(atoms, supercell_matrix) - return Material(from_ase(supercell_atoms)) + new_material = Material(from_ase(supercell_atoms)) + if material.metadata: + new_material.metadata = material.metadata + new_material.name = material.name + return new_material From 9e3642c1183c6c4d94c4ef8cb0db0d4a03861929 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:27:58 -0700 Subject: [PATCH 31/57] chore: fix lint --- src/py/mat3ra/made/tools/build/perturbation/builders.py | 3 +-- src/py/mat3ra/made/tools/build/supercell.py | 4 ++-- tests/py/unit/test_tools_build_perturbation.py | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/perturbation/builders.py b/src/py/mat3ra/made/tools/build/perturbation/builders.py index 8bdd6d19..2ed7dfaf 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/builders.py +++ b/src/py/mat3ra/made/tools/build/perturbation/builders.py @@ -11,7 +11,6 @@ class PerturbationBuilder(BaseBuilder): _ConfigurationType: type(PerturbationConfiguration) = PerturbationConfiguration # type: ignore _GeneratedItemType: Material = Material - _PostProcessParametersType = None @staticmethod def _prepare_material(configuration): @@ -35,7 +34,7 @@ def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemTyp return [new_material] def _post_process( - self, items: List[_GeneratedItemType], post_process_parameters: Optional[_PostProcessParametersType] + self, items: List[_GeneratedItemType], post_process_parameters: Optional[BaseBuilder._PostProcessParametersType] ) -> List[Material]: return [wrap_material(item) for item in items] diff --git a/src/py/mat3ra/made/tools/build/supercell.py b/src/py/mat3ra/made/tools/build/supercell.py index 1a58df84..1ab80e1d 100644 --- a/src/py/mat3ra/made/tools/build/supercell.py +++ b/src/py/mat3ra/made/tools/build/supercell.py @@ -2,9 +2,9 @@ import numpy as np from mat3ra.made.material import Material -from ..third_party import ASEAtoms, ase_make_supercell +from ..third_party import ase_make_supercell from ..utils import decorator_convert_2x2_to_3x3 -from ..convert import from_ase, decorator_convert_material_args_kwargs_to_atoms, to_ase +from ..convert import from_ase, to_ase @decorator_convert_2x2_to_3x3 diff --git a/tests/py/unit/test_tools_build_perturbation.py b/tests/py/unit/test_tools_build_perturbation.py index 347b7e4b..699330d7 100644 --- a/tests/py/unit/test_tools_build_perturbation.py +++ b/tests/py/unit/test_tools_build_perturbation.py @@ -4,6 +4,7 @@ from mat3ra.made.tools.build.supercell import create_supercell from mat3ra.made.tools.utils import PerturbationFunctionHolder from mat3ra.utils import assertion as assertion_utils + from .fixtures import GRAPHENE From 49e148b91b9ffea551abfe9a4c6a065f4db38856 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:34:20 -0700 Subject: [PATCH 32/57] chore: types fixes --- src/py/mat3ra/made/tools/build/perturbation/builders.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/perturbation/builders.py b/src/py/mat3ra/made/tools/build/perturbation/builders.py index 2ed7dfaf..6aa83e3c 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/builders.py +++ b/src/py/mat3ra/made/tools/build/perturbation/builders.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Optional, Any from mat3ra.made.material import Material from mat3ra.made.tools.build import BaseBuilder @@ -11,6 +11,7 @@ class PerturbationBuilder(BaseBuilder): _ConfigurationType: type(PerturbationConfiguration) = PerturbationConfiguration # type: ignore _GeneratedItemType: Material = Material + _PostProcessParametersType: Any = None @staticmethod def _prepare_material(configuration): @@ -34,7 +35,9 @@ def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemTyp return [new_material] def _post_process( - self, items: List[_GeneratedItemType], post_process_parameters: Optional[BaseBuilder._PostProcessParametersType] + self, + items: List[_GeneratedItemType], + post_process_parameters: Optional[_PostProcessParametersType], ) -> List[Material]: return [wrap_material(item) for item in items] From aa757fc3f66b3541c6a639d1eac79193ce0fae69 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Fri, 2 Aug 2024 18:50:07 -0700 Subject: [PATCH 33/57] update: add flag for ease of use --- .../made/tools/build/perturbation/__init__.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/py/mat3ra/made/tools/build/perturbation/__init__.py b/src/py/mat3ra/made/tools/build/perturbation/__init__.py index e69de29b..3f9df2e4 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/__init__.py +++ b/src/py/mat3ra/made/tools/build/perturbation/__init__.py @@ -0,0 +1,38 @@ +from typing import Union, Optional + +from mat3ra.made.material import Material +from .builders import ( + SlabPerturbationBuilder, + DistancePreservingSlabPerturbationBuilder, + CellMatchingDistancePreservingSlabPerturbationBuilder, +) +from .configuration import PerturbationConfiguration +from .builders import PerturbationFunctionHolder # type: ignore + + +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) From 460a321fb3de001843f8d5ed193d560ad6fabb44 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 4 Aug 2024 18:22:47 -0700 Subject: [PATCH 34/57] update: add a constructor from arrays --- src/py/mat3ra/made/lattice.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/py/mat3ra/made/lattice.py b/src/py/mat3ra/made/lattice.py index e6e4f442..32f196f0 100644 --- a/src/py/mat3ra/made/lattice.py +++ b/src/py/mat3ra/made/lattice.py @@ -52,16 +52,28 @@ def vectors(self) -> List[List[float]]: [0.0, 0.0, c], ] - def from_nested_array(self, vectors: List[List[float]]) -> "Lattice": + @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))) - return Lattice( - a=float(a), b=float(b), c=float(c), alpha=alpha, beta=beta, gamma=gamma, units=self.units, type=self.type - ) + if units is None: + units = cls.units + if type is None: + type = cls.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 @@ -89,7 +101,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) From 1eea22248fbaa107dfd9c270f1a6a25d6974cd56 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 4 Aug 2024 18:30:07 -0700 Subject: [PATCH 35/57] update: move coordinate related utils to a folder --- src/py/mat3ra/made/tools/modify.py | 2 +- .../tools/{utils.py => utils/__init__.py} | 160 +----------------- src/py/mat3ra/made/tools/utils/coordinate.py | 154 +++++++++++++++++ 3 files changed, 164 insertions(+), 152 deletions(-) rename src/py/mat3ra/made/tools/{utils.py => utils/__init__.py} (65%) create mode 100644 src/py/mat3ra/made/tools/utils/coordinate.py diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index 0d3d86f4..7604ff2a 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -9,7 +9,7 @@ ) from .convert import decorator_convert_material_args_kwargs_to_structure, from_ase, to_ase from .third_party import PymatgenStructure, ase_add_vacuum -from .utils import ( +from .utils.coordinate import ( is_coordinate_in_box, is_coordinate_in_cylinder, is_coordinate_in_triangular_prism, diff --git a/src/py/mat3ra/made/tools/utils.py b/src/py/mat3ra/made/tools/utils/__init__.py similarity index 65% rename from src/py/mat3ra/made/tools/utils.py rename to src/py/mat3ra/made/tools/utils/__init__.py index 4deaf4ff..efd99b8f 100644 --- a/src/py/mat3ra/made/tools/utils.py +++ b/src/py/mat3ra/made/tools/utils/__init__.py @@ -6,7 +6,15 @@ from scipy.integrate import quad from scipy.optimize import root_scalar -from .third_party import PymatgenStructure +from ..third_party import PymatgenStructure + +from .coordinate import ( + is_coordinate_behind_plane, + is_coordinate_in_box, + is_coordinate_in_cylinder, + is_coordinate_in_sphere, + is_coordinate_in_triangular_prism, +) DEFAULT_SCALING_FACTOR = np.array([3, 3, 3]) DEFAULT_TRANSLATION_VECTOR = 1 / DEFAULT_SCALING_FACTOR @@ -73,156 +81,6 @@ def get_norm(vector: List[float]) -> float: # Condition functions: -def is_coordinate_in_cylinder( - coordinate: List[float], center_position: List[float], radius: float = 0.25, min_z: float = 0, max_z: float = 1 -) -> bool: - """ - Check if a coordinate is inside a cylinder. - Args: - coordinate (List[float]): The coordinate to check. - center_position (List[float]): The coordinates of the center position. - min_z (float): Lower limit of z-coordinate. - max_z (float): Upper limit of z-coordinate. - radius (float): The radius of the 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 - ) - - -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: - """ - Check if a coordinate is inside a box. - Args: - coordinate (List[float]): The coordinate to check. - min_coordinate (List[float]): The minimum coordinate of the box. - max_coordinate (List[float]): The maximum coordinate of the 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 - - -def is_coordinate_within_layer( - coordinate: List[float], center_position: List[float], direction_vector: List[float], layer_thickness: float -) -> bool: - """ - Checks if a coordinate's projection along a specified direction vector - is within a certain layer thickness centered around a given position. - - Args: - coordinate (List[float]): The coordinate to check. - center_position (List[float]): The coordinates of the center position. - direction_vector (List[float]): The direction vector along which the layer thickness is defined. - layer_thickness (float): The thickness of the layer along the direction vector. - - Returns: - bool: True if the coordinate is within the layer thickness, False otherwise. - """ - direction_norm = np.array(direction_vector) / np.linalg.norm(direction_vector) - central_projection = np.dot(center_position, direction_norm) - layer_thickness_frac = layer_thickness / np.linalg.norm(direction_vector) - - lower_bound = central_projection - layer_thickness_frac / 2 - upper_bound = central_projection + layer_thickness_frac / 2 - - return lower_bound <= np.dot(coordinate, direction_norm) <= upper_bound - - -def is_coordinate_in_triangular_prism( - coordinate: List[float], - coordinate_1: List[float], - coordinate_2: List[float], - coordinate_3: List[float], - min_z: float = 0, - max_z: float = 1, -) -> bool: - """ - Check if a coordinate is inside a triangular prism. - Args: - coordinate (List[float]): The coordinate to check. - coordinate_1 (List[float]): The first coordinate of the triangle. - coordinate_2 (List[float]): The second coordinate of the triangle. - coordinate_3 (List[float]): The third coordinate of the triangle. - min_z (float): Lower limit of z-coordinate. - max_z (float): Upper limit of z-coordinate. - - Returns: - bool: True if the coordinate is inside the triangular prism, False otherwise. - """ - # convert to 3D coordinates at the origin XY plane - coordinate_1.extend([0] * (3 - len(coordinate_1))) - coordinate_2.extend([0] * (3 - len(coordinate_2))) - coordinate_3.extend([0] * (3 - len(coordinate_3))) - - np_coordinate = np.array(coordinate) - v1 = np.array(coordinate_1) - v2 = np.array(coordinate_2) - v3 = np.array(coordinate_3) - - v2_v1 = v2 - v1 - v3_v1 = v3 - v1 - coordinate_v1 = np_coordinate - v1 - - # Compute dot products for the barycentric coordinates - d00 = np.dot(v2_v1, v2_v1) - d01 = np.dot(v2_v1, v3_v1) - d11 = np.dot(v3_v1, v3_v1) - d20 = np.dot(coordinate_v1, v2_v1) - d21 = np.dot(coordinate_v1, v3_v1) - - # Calculate barycentric coordinates - denom = d00 * d11 - d01 * d01 - v = (d11 * d20 - d01 * d21) / denom - w = (d00 * d21 - d01 * d20) / denom - u = 1.0 - v - w - - return (u >= 0) and (v >= 0) and (w >= 0) and (u + v + w <= 1) and (min_z <= np_coordinate[2] <= max_z) - - -def is_coordinate_behind_plane( - coordinate: List[float], plane_normal: List[float], plane_point_coordinate: List[float] -) -> bool: - """ - Check if a coordinate is behind a plane. - Args: - coordinate (List[float]): The coordinate to check. - plane_normal (List[float]): The normal vector of the plane. - plane_point_coordinate (List[float]): The coordinate of a point on the plane. - - Returns: - bool: True if the coordinate is behind the plane, False otherwise. - """ - np_coordinate = np.array(coordinate) - np_plane_normal = np.array(plane_normal) - np_plane_point = np.array(plane_point_coordinate) - return np.dot(np_plane_normal, np_coordinate - np_plane_point) < 0 - - def transform_coordinate_to_supercell( coordinate: List[float], scaling_factor: Optional[List[int]] = None, diff --git a/src/py/mat3ra/made/tools/utils/coordinate.py b/src/py/mat3ra/made/tools/utils/coordinate.py new file mode 100644 index 00000000..e0baee19 --- /dev/null +++ b/src/py/mat3ra/made/tools/utils/coordinate.py @@ -0,0 +1,154 @@ +# Place all functions acting on coordinates +from typing import List + +import numpy as np + + +def is_coordinate_in_cylinder( + coordinate: List[float], center_position: List[float], radius: float = 0.25, min_z: float = 0, max_z: float = 1 +) -> bool: + """ + Check if a coordinate is inside a cylinder. + Args: + coordinate (List[float]): The coordinate to check. + center_position (List[float]): The coordinates of the center position. + min_z (float): Lower limit of z-coordinate. + max_z (float): Upper limit of z-coordinate. + radius (float): The radius of the 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 + ) + + +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: + """ + Check if a coordinate is inside a box. + Args: + coordinate (List[float]): The coordinate to check. + min_coordinate (List[float]): The minimum coordinate of the box. + max_coordinate (List[float]): The maximum coordinate of the 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 + + +def is_coordinate_within_layer( + coordinate: List[float], center_position: List[float], direction_vector: List[float], layer_thickness: float +) -> bool: + """ + Checks if a coordinate's projection along a specified direction vector + is within a certain layer thickness centered around a given position. + + Args: + coordinate (List[float]): The coordinate to check. + center_position (List[float]): The coordinates of the center position. + direction_vector (List[float]): The direction vector along which the layer thickness is defined. + layer_thickness (float): The thickness of the layer along the direction vector. + + Returns: + bool: True if the coordinate is within the layer thickness, False otherwise. + """ + direction_norm = np.array(direction_vector) / np.linalg.norm(direction_vector) + central_projection = np.dot(center_position, direction_norm) + layer_thickness_frac = layer_thickness / np.linalg.norm(direction_vector) + + lower_bound = central_projection - layer_thickness_frac / 2 + upper_bound = central_projection + layer_thickness_frac / 2 + + return lower_bound <= np.dot(coordinate, direction_norm) <= upper_bound + + +def is_coordinate_in_triangular_prism( + coordinate: List[float], + coordinate_1: List[float], + coordinate_2: List[float], + coordinate_3: List[float], + min_z: float = 0, + max_z: float = 1, +) -> bool: + """ + Check if a coordinate is inside a triangular prism. + Args: + coordinate (List[float]): The coordinate to check. + coordinate_1 (List[float]): The first coordinate of the triangle. + coordinate_2 (List[float]): The second coordinate of the triangle. + coordinate_3 (List[float]): The third coordinate of the triangle. + min_z (float): Lower limit of z-coordinate. + max_z (float): Upper limit of z-coordinate. + + Returns: + bool: True if the coordinate is inside the triangular prism, False otherwise. + """ + # convert to 3D coordinates at the origin XY plane + coordinate_1.extend([0] * (3 - len(coordinate_1))) + coordinate_2.extend([0] * (3 - len(coordinate_2))) + coordinate_3.extend([0] * (3 - len(coordinate_3))) + + np_coordinate = np.array(coordinate) + v1 = np.array(coordinate_1) + v2 = np.array(coordinate_2) + v3 = np.array(coordinate_3) + + v2_v1 = v2 - v1 + v3_v1 = v3 - v1 + coordinate_v1 = np_coordinate - v1 + + # Compute dot products for the barycentric coordinates + d00 = np.dot(v2_v1, v2_v1) + d01 = np.dot(v2_v1, v3_v1) + d11 = np.dot(v3_v1, v3_v1) + d20 = np.dot(coordinate_v1, v2_v1) + d21 = np.dot(coordinate_v1, v3_v1) + + # Calculate barycentric coordinates + denom = d00 * d11 - d01 * d01 + v = (d11 * d20 - d01 * d21) / denom + w = (d00 * d21 - d01 * d20) / denom + u = 1.0 - v - w + + return (u >= 0) and (v >= 0) and (w >= 0) and (u + v + w <= 1) and (min_z <= np_coordinate[2] <= max_z) + + +def is_coordinate_behind_plane( + coordinate: List[float], plane_normal: List[float], plane_point_coordinate: List[float] +) -> bool: + """ + Check if a coordinate is behind a plane. + Args: + coordinate (List[float]): The coordinate to check. + plane_normal (List[float]): The normal vector of the plane. + plane_point_coordinate (List[float]): The coordinate of a point on the plane. + + Returns: + bool: True if the coordinate is behind the plane, False otherwise. + """ + np_coordinate = np.array(coordinate) + np_plane_normal = np.array(plane_normal) + np_plane_point = np.array(plane_point_coordinate) + return np.dot(np_plane_normal, np_coordinate - np_plane_point) < 0 From ee92a581d119c289c99efde4e4e8d802ec04d5a0 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 4 Aug 2024 18:33:15 -0700 Subject: [PATCH 36/57] update: rename nested_array -> vectors_array --- src/py/mat3ra/made/basis.py | 2 +- src/py/mat3ra/made/cell.py | 18 +++++++++--------- .../mat3ra/made/tools/build/defect/builders.py | 4 ++-- .../made/tools/build/perturbation/builders.py | 6 +++--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/py/mat3ra/made/basis.py b/src/py/mat3ra/made/basis.py index 298e9af1..b86dbd71 100644 --- a/src/py/mat3ra/made/basis.py +++ b/src/py/mat3ra/made/basis.py @@ -31,7 +31,7 @@ def from_dict( 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=[]), ) diff --git a/src/py/mat3ra/made/cell.py b/src/py/mat3ra/made/cell.py index 0f65bdbb..e3b585f4 100644 --- a/src/py/mat3ra/made/cell.py +++ b/src/py/mat3ra/made/cell.py @@ -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): + 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]) @@ -33,7 +33,7 @@ def to_json(self, skip_rounding=False): ] def clone(self): - return self.from_nested_array(self.vectors_as_nested_array) + return self.from_vectors_array(self.vectors_as_array) def clone_and_scale_by_matrix(self, matrix): new_cell = self.clone() @@ -41,13 +41,13 @@ def clone_and_scale_by_matrix(self, matrix): return new_cell def convert_point_to_cartesian(self, point): - np_vector = np.array(self.vectors_as_nested_array) + 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) + 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) + np_vector = np.array(self.vectors_as_array) self.vector1, self.vector2, self.vector3 = np.dot(matrix, np_vector).tolist() diff --git a/src/py/mat3ra/made/tools/build/defect/builders.py b/src/py/mat3ra/made/tools/build/defect/builders.py index 7d8ee877..4346eca4 100644 --- a/src/py/mat3ra/made/tools/build/defect/builders.py +++ b/src/py/mat3ra/made/tools/build/defect/builders.py @@ -463,7 +463,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 @@ -499,7 +499,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]) diff --git a/src/py/mat3ra/made/tools/build/perturbation/builders.py b/src/py/mat3ra/made/tools/build/perturbation/builders.py index 6aa83e3c..1e9a15e9 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/builders.py +++ b/src/py/mat3ra/made/tools/build/perturbation/builders.py @@ -73,19 +73,19 @@ class CellMatchingDistancePreservingSlabPerturbationBuilder(DistancePreservingSl def _transform_cell_vectors(self, configuration: PerturbationConfiguration) -> List[List[float]]: perturbation_function, perturbation_json = configuration.perturbation_function coord_transformation_function = PerturbationFunctionHolder.get_coord_transformation(perturbation_json) - cell_vectors = configuration.material.basis.cell.vectors_as_nested_array + cell_vectors = configuration.material.basis.cell.vectors_as_array return [perturbation_function(coord_transformation_function(coord)) for coord in cell_vectors] def create_perturbed_slab(self, configuration: PerturbationConfiguration): new_material = super().create_perturbed_slab(configuration) new_lattice_vectors = self._transform_cell_vectors(configuration) new_lattice = new_material.lattice.copy() - new_lattice = new_lattice.from_nested_array(new_lattice_vectors) + new_lattice = new_lattice.from_vectors_array(new_lattice_vectors) new_material.lattice = new_lattice new_basis = new_material.basis.copy() new_basis.to_cartesian() - new_basis.cell = new_basis.cell.from_nested_array(new_lattice_vectors) + new_basis.cell = new_basis.cell.from_vectors_array(new_lattice_vectors) new_basis.to_crystal() new_material.basis = new_basis return new_material From 9d4c88286b492ecfde4d1ba822f8d3a0a9b92cf1 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 5 Aug 2024 21:15:56 -0700 Subject: [PATCH 37/57] update: move defs from defs --- src/py/mat3ra/made/tools/utils/__init__.py | 29 ++++++++++++---------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/py/mat3ra/made/tools/utils/__init__.py b/src/py/mat3ra/made/tools/utils/__init__.py index efd99b8f..d1ef8fdc 100644 --- a/src/py/mat3ra/made/tools/utils/__init__.py +++ b/src/py/mat3ra/made/tools/utils/__init__.py @@ -2,7 +2,9 @@ from typing import Callable, Dict, List, Literal, Optional, Tuple import numpy as np +import sympy as sp from mat3ra.utils.matrix import convert_2x2_to_3x3 +from pydantic import BaseModel from scipy.integrate import quad from scipy.optimize import root_scalar @@ -226,6 +228,19 @@ def deformation(coordinate: List[float]): return deformation, config + @staticmethod + def sine_wave_diff(w: float, amplitude: float, wavelength: float, phase: float) -> float: + return amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * w / wavelength + phase) + + @staticmethod + def sine_wave_length_integral_equation(w_prime: float, w: float, amplitude: float, wavelength: float, phase: float): + arc_length = quad( + lambda t: np.sqrt(1 + (PerturbationFunctionHolder.sine_wave_diff(t, amplitude, wavelength, phase)) ** 2), + a=0, + b=w_prime, + )[0] + return arc_length - w + @staticmethod def sine_wave_transform_coordinates( amplitude: float, wavelength: float, phase: float, axis: Literal["x", "y"] @@ -243,25 +258,13 @@ def sine_wave_transform_coordinates( Returns: Callable[[List[float]], List[float]]: The coordinates transformation function. """ - - def sine_wave_diff(w: float, amplitude: float, wavelength: float, phase: float) -> float: - return amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * w / wavelength + phase) - - def sine_wave_length_integral_equation( - w_prime: float, w: float, amplitude: float, wavelength: float, phase: float - ): - arc_length = quad( - lambda t: np.sqrt(1 + (sine_wave_diff(t, amplitude, wavelength, phase)) ** 2), a=0, b=w_prime - )[0] - return arc_length - w - index = AXIS_TO_INDEX_MAP[axis] def coordinate_transformation(coordinate: List[float]): w = coordinate[index] # Find x' such that the integral from 0 to x' equals x result = root_scalar( - sine_wave_length_integral_equation, + PerturbationFunctionHolder.sine_wave_length_integral_equation, args=(w, amplitude, wavelength, phase), bracket=[0, EQUATION_RANGE_COEFFICIENT * w], method="brentq", From 427994e118dba5cf1ded97ddba86c8c1d4f35a94 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 5 Aug 2024 21:16:13 -0700 Subject: [PATCH 38/57] update: minor fix --- src/py/mat3ra/made/lattice.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/py/mat3ra/made/lattice.py b/src/py/mat3ra/made/lattice.py index 32f196f0..e6fe59a4 100644 --- a/src/py/mat3ra/made/lattice.py +++ b/src/py/mat3ra/made/lattice.py @@ -8,6 +8,8 @@ from .cell import Cell HASH_TOLERANCE = 3 +DEFAULT_UNITS = {"length": "angstrom", "angle": "degree"} +DEFAULT_TYPE = "TRI" class Lattice(RoundNumericValuesMixin, BaseModel): @@ -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]]: @@ -70,9 +69,9 @@ def from_vectors_array( 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 = cls.units + units = DEFAULT_UNITS if type is None: - type = cls.type + 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]: From bf561bbde7d95760bf552bae4d87fde50d4fe3ac Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 5 Aug 2024 21:17:06 -0700 Subject: [PATCH 39/57] chore: run lint fix --- src/py/mat3ra/made/tools/utils/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/py/mat3ra/made/tools/utils/__init__.py b/src/py/mat3ra/made/tools/utils/__init__.py index d1ef8fdc..e3a13422 100644 --- a/src/py/mat3ra/made/tools/utils/__init__.py +++ b/src/py/mat3ra/made/tools/utils/__init__.py @@ -2,14 +2,11 @@ from typing import Callable, Dict, List, Literal, Optional, Tuple import numpy as np -import sympy as sp from mat3ra.utils.matrix import convert_2x2_to_3x3 -from pydantic import BaseModel from scipy.integrate import quad from scipy.optimize import root_scalar from ..third_party import PymatgenStructure - from .coordinate import ( is_coordinate_behind_plane, is_coordinate_in_box, From bccdeb13875ef63ed8f4c18dae956c228fe490ad Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 5 Aug 2024 22:16:25 -0700 Subject: [PATCH 40/57] update: move sine realated and add factory --- src/py/mat3ra/made/tools/utils/__init__.py | 58 ++--------------- src/py/mat3ra/made/tools/utils/factories.py | 7 ++ src/py/mat3ra/made/tools/utils/helpers.py | 71 +++++++++++++++++++++ 3 files changed, 84 insertions(+), 52 deletions(-) create mode 100644 src/py/mat3ra/made/tools/utils/factories.py create mode 100644 src/py/mat3ra/made/tools/utils/helpers.py diff --git a/src/py/mat3ra/made/tools/utils/__init__.py b/src/py/mat3ra/made/tools/utils/__init__.py index e3a13422..25f5fcaf 100644 --- a/src/py/mat3ra/made/tools/utils/__init__.py +++ b/src/py/mat3ra/made/tools/utils/__init__.py @@ -2,9 +2,8 @@ from typing import Callable, Dict, List, Literal, Optional, Tuple import numpy as np +from mat3ra.utils.factory import BaseFactory from mat3ra.utils.matrix import convert_2x2_to_3x3 -from scipy.integrate import quad -from scipy.optimize import root_scalar from ..third_party import PymatgenStructure from .coordinate import ( @@ -14,11 +13,11 @@ is_coordinate_in_sphere, is_coordinate_in_triangular_prism, ) +from .factories import PerturbationFunctionHelperFactory +from .helpers import AXIS_TO_INDEX_MAP DEFAULT_SCALING_FACTOR = np.array([3, 3, 3]) DEFAULT_TRANSLATION_VECTOR = 1 / DEFAULT_SCALING_FACTOR -AXIS_TO_INDEX_MAP = {"x": 0, "y": 1, "z": 2} -EQUATION_RANGE_COEFFICIENT = 5 # TODO: convert to accept ASE Atoms object @@ -190,8 +189,9 @@ class PerturbationFunctionHolder: def get_coord_transformation(perturbation_json: dict) -> Callable: new_perturbation_json = perturbation_json.copy() name = new_perturbation_json.pop("type") - name_to_function_map = {"sine_wave": PerturbationFunctionHolder.sine_wave_transform_coordinates} - return name_to_function_map[name](**new_perturbation_json) + helper_function = PerturbationFunctionHelperFactory.get_class_by_name(name) + # TODO: add type of SineWave (or corresponding one to the return of the factory) + return helper_function.get_transform_coordinates(**new_perturbation_json) @staticmethod def sine_wave( @@ -225,52 +225,6 @@ def deformation(coordinate: List[float]): return deformation, config - @staticmethod - def sine_wave_diff(w: float, amplitude: float, wavelength: float, phase: float) -> float: - return amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * w / wavelength + phase) - - @staticmethod - def sine_wave_length_integral_equation(w_prime: float, w: float, amplitude: float, wavelength: float, phase: float): - arc_length = quad( - lambda t: np.sqrt(1 + (PerturbationFunctionHolder.sine_wave_diff(t, amplitude, wavelength, phase)) ** 2), - a=0, - b=w_prime, - )[0] - return arc_length - w - - @staticmethod - def sine_wave_transform_coordinates( - amplitude: float, wavelength: float, phase: float, axis: Literal["x", "y"] - ) -> Callable[[List[float]], List[float]]: - """ - Transform coordinates to preserve the distance between points on a sine wave. - Achieved by calculating the integral of the length between [0,0,0] and given coordinate. - - Args: - amplitude (float): The amplitude of the sine wave in cartesian coordinates. - wavelength (float): The wavelength of the sine wave in cartesian coordinates. - phase (float): The phase of the sine wave in cartesian coordinates. - axis (str): The axis of the direction of the sine wave. - - Returns: - Callable[[List[float]], List[float]]: The coordinates transformation function. - """ - index = AXIS_TO_INDEX_MAP[axis] - - def coordinate_transformation(coordinate: List[float]): - w = coordinate[index] - # Find x' such that the integral from 0 to x' equals x - result = root_scalar( - PerturbationFunctionHolder.sine_wave_length_integral_equation, - args=(w, amplitude, wavelength, phase), - bracket=[0, EQUATION_RANGE_COEFFICIENT * w], - method="brentq", - ) - coordinate[index] = result.root - return coordinate - - return coordinate_transformation - @staticmethod def sine_wave_radial( amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, center_position=None diff --git a/src/py/mat3ra/made/tools/utils/factories.py b/src/py/mat3ra/made/tools/utils/factories.py new file mode 100644 index 00000000..4b74f5cd --- /dev/null +++ b/src/py/mat3ra/made/tools/utils/factories.py @@ -0,0 +1,7 @@ +from mat3ra.utils.factory import BaseFactory + + +class PerturbationFunctionHelperFactory(BaseFactory): + __class_registry__ = { + "sine_wave": "mat3ra.made.tools.utils.helpers.SineWave", + } diff --git a/src/py/mat3ra/made/tools/utils/helpers.py b/src/py/mat3ra/made/tools/utils/helpers.py new file mode 100644 index 00000000..019b2bff --- /dev/null +++ b/src/py/mat3ra/made/tools/utils/helpers.py @@ -0,0 +1,71 @@ +from typing import Callable, List, Literal + +import numpy as np +from pydantic import BaseModel +from scipy.integrate import quad +from scipy.optimize import root_scalar + +AXIS_TO_INDEX_MAP = {"x": 0, "y": 1, "z": 2} +EQUATION_RANGE_COEFFICIENT = 5 + + +class FunctionHolder(BaseModel): + @staticmethod + def get_derivative(*args, **kwargs): + raise NotImplementedError + + @staticmethod + def get_arc_length_equation(*args, **kwargs): + raise NotImplementedError + + @staticmethod + def get_transform_coordinates(*args, **kwargs): + raise NotImplementedError + + +class SineWave(FunctionHolder): + @staticmethod + def get_derivative(w: float, amplitude: float, wavelength: float, phase: float) -> float: + return amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * w / wavelength + phase) + + @staticmethod + def get_arc_length_equation(w_prime: float, w: float, amplitude: float, wavelength: float, phase: float) -> float: + arc_length = quad( + lambda t: np.sqrt(1 + (SineWave.get_derivative(t, amplitude, wavelength, phase)) ** 2), + a=0, + b=w_prime, + )[0] + return arc_length - w + + @staticmethod + def get_transform_coordinates( + amplitude: float, wavelength: float, phase: float, axis: Literal["x", "y"] + ) -> Callable[[List[float]], List[float]]: + """ + Transform coordinates to preserve the distance between points on a sine wave. + Achieved by calculating the integral of the length between [0,0,0] and given coordinate. + + Args: + amplitude (float): The amplitude of the sine wave in cartesian coordinates. + wavelength (float): The wavelength of the sine wave in cartesian coordinates. + phase (float): The phase of the sine wave in cartesian coordinates. + axis (str): The axis of the direction of the sine wave. + + Returns: + Callable[[List[float]], List[float]]: The coordinates transformation function. + """ + index = AXIS_TO_INDEX_MAP[axis] + + def coordinate_transformation(coordinate: List[float]): + w = coordinate[index] + # Find x' such that the integral from 0 to x' equals x + result = root_scalar( + SineWave.get_arc_length_equation, + args=(w, amplitude, wavelength, phase), + bracket=[0, EQUATION_RANGE_COEFFICIENT * w], + method="brentq", + ) + coordinate[index] = result.root + return coordinate + + return coordinate_transformation From 88d95cd32b2e1da6d28547fa68799b0fce5f9d5d Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:34:46 -0700 Subject: [PATCH 41/57] update: cleanup --- src/py/mat3ra/made/tools/utils/helpers.py | 41 ++++++++++++++++------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/py/mat3ra/made/tools/utils/helpers.py b/src/py/mat3ra/made/tools/utils/helpers.py index 019b2bff..17e2aad8 100644 --- a/src/py/mat3ra/made/tools/utils/helpers.py +++ b/src/py/mat3ra/made/tools/utils/helpers.py @@ -10,20 +10,48 @@ class FunctionHolder(BaseModel): + + @staticmethod + def get_function(*args, **kwargs): + """ + Get the function of the perturbation. + """ + raise NotImplementedError + @staticmethod def get_derivative(*args, **kwargs): + """ + Get the derivative of the perturbation function. + """ raise NotImplementedError @staticmethod def get_arc_length_equation(*args, **kwargs): + """ + Get the equation to calculate the arc length between [0,0,0] and a given coordinate of the perturbation. + """ raise NotImplementedError @staticmethod def get_transform_coordinates(*args, **kwargs): + """ + Transform coordinates to preserve the distance between points on a sine wave when perturbation is applied. + Achieved by calculating the integral of the length between [0,0,0] and given coordinate. + + Returns: + Callable[[List[float]], List[float]]: The coordinates transformation function. + """ raise NotImplementedError class SineWave(FunctionHolder): + + @staticmethod + def get_function( + w: float, amplitude: float, wavelength: float, phase: float + ) -> Callable[[List[float]], List[float]]: + return amplitude * np.sin(2 * np.pi * w / wavelength + phase) + @staticmethod def get_derivative(w: float, amplitude: float, wavelength: float, phase: float) -> float: return amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * w / wavelength + phase) @@ -41,19 +69,6 @@ def get_arc_length_equation(w_prime: float, w: float, amplitude: float, waveleng def get_transform_coordinates( amplitude: float, wavelength: float, phase: float, axis: Literal["x", "y"] ) -> Callable[[List[float]], List[float]]: - """ - Transform coordinates to preserve the distance between points on a sine wave. - Achieved by calculating the integral of the length between [0,0,0] and given coordinate. - - Args: - amplitude (float): The amplitude of the sine wave in cartesian coordinates. - wavelength (float): The wavelength of the sine wave in cartesian coordinates. - phase (float): The phase of the sine wave in cartesian coordinates. - axis (str): The axis of the direction of the sine wave. - - Returns: - Callable[[List[float]], List[float]]: The coordinates transformation function. - """ index = AXIS_TO_INDEX_MAP[axis] def coordinate_transformation(coordinate: List[float]): From b34ae4cf97396c1bab1d7f743c10d7c379d9e15c Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:35:38 -0700 Subject: [PATCH 42/57] update: use factory for pertrubation --- src/py/mat3ra/made/tools/utils/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/py/mat3ra/made/tools/utils/__init__.py b/src/py/mat3ra/made/tools/utils/__init__.py index 25f5fcaf..52124565 100644 --- a/src/py/mat3ra/made/tools/utils/__init__.py +++ b/src/py/mat3ra/made/tools/utils/__init__.py @@ -2,7 +2,6 @@ from typing import Callable, Dict, List, Literal, Optional, Tuple import numpy as np -from mat3ra.utils.factory import BaseFactory from mat3ra.utils.matrix import convert_2x2_to_3x3 from ..third_party import PymatgenStructure @@ -213,17 +212,18 @@ def sine_wave( """ if axis in AXIS_TO_INDEX_MAP: index = AXIS_TO_INDEX_MAP[axis] + perturbation_function = PerturbationFunctionHelperFactory.get_class_by_name("sine_wave") - def deformation(coordinate: List[float]): + def perturbation(coordinate: List[float]): return [ coordinate[0], coordinate[1], - coordinate[2] + amplitude * np.sin(2 * np.pi * coordinate[index] / wavelength + phase), + coordinate[2] + perturbation_function.get_function(coordinate[index], amplitude, wavelength, phase), ] config = {"type": "sine_wave", "amplitude": amplitude, "wavelength": wavelength, "phase": phase, "axis": axis} - return deformation, config + return perturbation, config @staticmethod def sine_wave_radial( @@ -243,7 +243,7 @@ def sine_wave_radial( if center_position is None: center_position = [0.5, 0.5] - def radial_sine_wave(coordinate: List[float]): + def perturbation(coordinate: List[float]): np_position = np.array(coordinate[:2]) np_center_position = np.array(center_position) distance = np.linalg.norm(np_position - np_center_position) @@ -261,4 +261,4 @@ def radial_sine_wave(coordinate: List[float]): "center_position": center_position, } - return radial_sine_wave, config + return perturbation, config From 37db0515ac892e591840e96dcf1c0951594d8a2c Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:36:06 -0700 Subject: [PATCH 43/57] chore: run lint fix --- src/py/mat3ra/made/tools/utils/helpers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/utils/helpers.py b/src/py/mat3ra/made/tools/utils/helpers.py index 17e2aad8..aba85dd3 100644 --- a/src/py/mat3ra/made/tools/utils/helpers.py +++ b/src/py/mat3ra/made/tools/utils/helpers.py @@ -10,7 +10,6 @@ class FunctionHolder(BaseModel): - @staticmethod def get_function(*args, **kwargs): """ @@ -45,7 +44,6 @@ def get_transform_coordinates(*args, **kwargs): class SineWave(FunctionHolder): - @staticmethod def get_function( w: float, amplitude: float, wavelength: float, phase: float From 3806063e526327850c7d4a1d7a250c5093dd12ce Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:52:52 -0700 Subject: [PATCH 44/57] update: add test for distance preserving perturbation --- .../py/unit/test_tools_build_perturbation.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/py/unit/test_tools_build_perturbation.py b/tests/py/unit/test_tools_build_perturbation.py index 699330d7..69cef4e5 100644 --- a/tests/py/unit/test_tools_build_perturbation.py +++ b/tests/py/unit/test_tools_build_perturbation.py @@ -1,4 +1,6 @@ +from mat3ra.made.cell import Cell from mat3ra.made.material import Material +from mat3ra.made.tools.build.perturbation import create_perturbation from mat3ra.made.tools.build.perturbation.builders import SlabPerturbationBuilder from mat3ra.made.tools.build.perturbation.configuration import PerturbationConfiguration from mat3ra.made.tools.build.supercell import create_supercell @@ -22,3 +24,27 @@ def test_sine_perturbation(): # Check selected atoms to avoid using 100+ atoms fixture assertion_utils.assert_deep_almost_equal([0.0, 0.0, 0.5], perturbed_slab.basis.coordinates.values[0]) assertion_utils.assert_deep_almost_equal([0.2, 0.1, 0.547552826], perturbed_slab.basis.coordinates.values[42]) + + +def test_distance_preserved_sine_perturbation(): + material = Material(GRAPHENE) + slab = create_supercell(material, [[10, 0, 0], [0, 10, 0], [0, 0, 1]]) + + perturbation_config = PerturbationConfiguration( + material=slab, + perturbation_function=PerturbationFunctionHolder.sine_wave(amplitude=0.05, wavelength=1, phase=0.25, axis="y"), + use_cartesian_coordinates=False, + ) + perturbed_slab = create_perturbation(configuration=perturbation_config, preserve_distance=True) + # Check selected atoms to avoid using 100+ atoms fixture + assertion_utils.assert_deep_almost_equal([0.0, 0.0, 0.512053493], perturbed_slab.basis.coordinates.values[0]) + assertion_utils.assert_deep_almost_equal( + [0.201165188, 0.099003051, 0.537490872], perturbed_slab.basis.coordinates.values[42] + ) + # Value taken from visually inspected notebook + expected_cell = Cell( + vector1=[24.67291, 0.0, 0.012370198], + vector2=[-12.336455, 20.864413342, -0.028311143], + vector3=[0.0, 0.0, 20.012370198], + ) + assertion_utils.assert_deep_almost_equal(expected_cell.vectors_as_array, perturbed_slab.basis.cell.vectors_as_array) From ccf97021d3de171079fc681c55f447ee33f15b0d Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:24:02 -0700 Subject: [PATCH 45/57] update: add json as method: --- src/py/mat3ra/made/tools/utils/__init__.py | 41 +--------------------- src/py/mat3ra/made/tools/utils/helpers.py | 11 ++++++ 2 files changed, 12 insertions(+), 40 deletions(-) diff --git a/src/py/mat3ra/made/tools/utils/__init__.py b/src/py/mat3ra/made/tools/utils/__init__.py index 52124565..6923da58 100644 --- a/src/py/mat3ra/made/tools/utils/__init__.py +++ b/src/py/mat3ra/made/tools/utils/__init__.py @@ -221,44 +221,5 @@ def perturbation(coordinate: List[float]): coordinate[2] + perturbation_function.get_function(coordinate[index], amplitude, wavelength, phase), ] - config = {"type": "sine_wave", "amplitude": amplitude, "wavelength": wavelength, "phase": phase, "axis": axis} - - return perturbation, config - - @staticmethod - def sine_wave_radial( - amplitude: float = 0.1, wavelength: float = 1, phase: float = 0, center_position=None - ) -> Tuple[Callable[[List[float]], List[float]], Dict]: - """ - Deform a coordinate using a radial sine wave. - Args: - amplitude (float): The amplitude of the sine wave in cartesian coordinates. - wavelength (float): The wavelength of the sine wave in cartesian coordinates. - phase (float): The phase of the sine wave in cartesian coordinates. - center_position (List[float]): The center position of the sine wave on the plane. - - Returns: - Tuple[Callable[[List[float]], List[float]], Dict]: The perturbation function and its configuration - """ - if center_position is None: - center_position = [0.5, 0.5] - - def perturbation(coordinate: List[float]): - np_position = np.array(coordinate[:2]) - np_center_position = np.array(center_position) - distance = np.linalg.norm(np_position - np_center_position) - return [ - coordinate[0], - coordinate[1], - coordinate[2] + amplitude * np.sin(2 * np.pi * distance / wavelength + phase), - ] - - config = { - "type": "sine_wave_radial", - "amplitude": amplitude, - "wavelength": wavelength, - "phase": phase, - "center_position": center_position, - } - + config = perturbation_function.get_json(amplitude, wavelength, phase, axis) return perturbation, config diff --git a/src/py/mat3ra/made/tools/utils/helpers.py b/src/py/mat3ra/made/tools/utils/helpers.py index aba85dd3..0fc3675a 100644 --- a/src/py/mat3ra/made/tools/utils/helpers.py +++ b/src/py/mat3ra/made/tools/utils/helpers.py @@ -42,6 +42,13 @@ def get_transform_coordinates(*args, **kwargs): """ raise NotImplementedError + @staticmethod + def get_json(*args, **kwargs): + """ + Get the json representation of the perturbation. + """ + raise NotImplementedError + class SineWave(FunctionHolder): @staticmethod @@ -82,3 +89,7 @@ def coordinate_transformation(coordinate: List[float]): return coordinate return coordinate_transformation + + @staticmethod + def get_json(amplitude: float, wavelength: float, phase: float, axis: Literal["x", "y"]) -> dict: + return {"type": "sine_wave", "amplitude": amplitude, "wavelength": wavelength, "phase": phase, "axis": axis} From ae3cdcb12acf377f3c7c845b8c6551a2da957994 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:37:13 -0700 Subject: [PATCH 46/57] chore: add types --- src/py/mat3ra/made/cell.py | 16 ++++++++-------- .../made/tools/build/interface/builders.py | 10 ++++++---- .../made/tools/build/perturbation/builders.py | 18 +++++++++--------- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/py/mat3ra/made/cell.py b/src/py/mat3ra/made/cell.py index e3b585f4..01dd9558 100644 --- a/src/py/mat3ra/made/cell.py +++ b/src/py/mat3ra/made/cell.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional import numpy as np from mat3ra.utils.mixins import RoundNumericValuesMixin @@ -13,7 +13,7 @@ class Cell(RoundNumericValuesMixin, BaseModel): __round_precision__ = 6 @classmethod - def from_vectors_array(cls, vectors_array): + 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]) @@ -32,22 +32,22 @@ def to_json(self, skip_rounding=False): self.vector3 if skip_rounding else _(self.vector3), ] - def clone(self): + 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): + 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): + 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): + 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(matrix, np_vector).tolist() + self.vector1, self.vector2, self.vector3 = np.dot(np.array(matrix), np_vector).tolist() diff --git a/src/py/mat3ra/made/tools/build/interface/builders.py b/src/py/mat3ra/made/tools/build/interface/builders.py index a26df9c6..a02abbdf 100644 --- a/src/py/mat3ra/made/tools/build/interface/builders.py +++ b/src/py/mat3ra/made/tools/build/interface/builders.py @@ -145,11 +145,13 @@ class ZSLStrainMatchingInterfaceBuilder(ConvertGeneratedItemsPymatgenStructureMi def _generate(self, configuration: InterfaceConfiguration) -> List[PymatgenInterface]: generator = ZSLGenerator(**self.build_parameters.strain_matching_parameters.dict()) - substrate_down = translate_to_z_level(configuration.substrate_configuration.bulk, "bottom") - film_down = translate_to_z_level(configuration.film_configuration.bulk, "bottom") + 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(substrate_down), - film_structure=to_pymatgen(film_down), + 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, diff --git a/src/py/mat3ra/made/tools/build/perturbation/builders.py b/src/py/mat3ra/made/tools/build/perturbation/builders.py index 1e9a15e9..e219ab6d 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/builders.py +++ b/src/py/mat3ra/made/tools/build/perturbation/builders.py @@ -4,7 +4,7 @@ from mat3ra.made.tools.build import BaseBuilder from .configuration import PerturbationConfiguration -from ...modify import wrap_material, translate_to_z_level +from ...modify import wrap_to_unit_cell, translate_to_z_level from ...utils import PerturbationFunctionHolder @@ -14,7 +14,7 @@ class PerturbationBuilder(BaseBuilder): _PostProcessParametersType: Any = None @staticmethod - def _prepare_material(configuration): + def _prepare_material(configuration: _ConfigurationType) -> _GeneratedItemType: new_material = configuration.material.clone() new_material = translate_to_z_level(new_material, "center") if configuration.use_cartesian_coordinates: @@ -22,7 +22,7 @@ def _prepare_material(configuration): return new_material @staticmethod - def _set_new_coordinates(new_material, new_coordinates): + def _set_new_coordinates(new_material: Material, new_coordinates: List[List[float]]) -> Material: new_basis = new_material.basis.copy() new_basis.coordinates.values = new_coordinates new_basis.to_crystal() @@ -39,7 +39,7 @@ def _post_process( items: List[_GeneratedItemType], post_process_parameters: Optional[_PostProcessParametersType], ) -> List[Material]: - return [wrap_material(item) for item in items] + return [wrap_to_unit_cell(item) for item in items] def _update_material_name(self, material: Material, configuration: _ConfigurationType) -> Material: perturbation_details = f"Perturbation: {configuration.perturbation_function[1].get('type')}" @@ -48,7 +48,7 @@ def _update_material_name(self, material: Material, configuration: _Configuratio class SlabPerturbationBuilder(PerturbationBuilder): - def create_perturbed_slab(self, configuration): + def create_perturbed_slab(self, configuration: PerturbationConfiguration) -> Material: new_material = self._prepare_material(configuration) perturbation_function, _ = configuration.perturbation_function new_coordinates = [perturbation_function(coord) for coord in new_material.basis.coordinates.values] @@ -57,7 +57,7 @@ def create_perturbed_slab(self, configuration): class DistancePreservingSlabPerturbationBuilder(SlabPerturbationBuilder): - def create_perturbed_slab(self, configuration): + def create_perturbed_slab(self, configuration: PerturbationConfiguration) -> Material: new_material = self._prepare_material(configuration) perturbation_function, perturbation_json = configuration.perturbation_function coord_transformation_function = PerturbationFunctionHolder.get_coord_transformation(perturbation_json) @@ -70,15 +70,15 @@ def create_perturbed_slab(self, configuration): class CellMatchingDistancePreservingSlabPerturbationBuilder(DistancePreservingSlabPerturbationBuilder): - def _transform_cell_vectors(self, configuration: PerturbationConfiguration) -> List[List[float]]: + def _transform_lattice_vectors(self, configuration: PerturbationConfiguration) -> List[List[float]]: perturbation_function, perturbation_json = configuration.perturbation_function coord_transformation_function = PerturbationFunctionHolder.get_coord_transformation(perturbation_json) cell_vectors = configuration.material.basis.cell.vectors_as_array return [perturbation_function(coord_transformation_function(coord)) for coord in cell_vectors] - def create_perturbed_slab(self, configuration: PerturbationConfiguration): + def create_perturbed_slab(self, configuration: PerturbationConfiguration) -> Material: new_material = super().create_perturbed_slab(configuration) - new_lattice_vectors = self._transform_cell_vectors(configuration) + new_lattice_vectors = self._transform_lattice_vectors(configuration) new_lattice = new_material.lattice.copy() new_lattice = new_lattice.from_vectors_array(new_lattice_vectors) new_material.lattice = new_lattice From 5fca5dcb7f6fc22df8c851bdacdea5e8577efc08 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:37:33 -0700 Subject: [PATCH 47/57] chore: remove duplicated method --- src/py/mat3ra/made/tools/modify.py | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index 7604ff2a..bbeb8064 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -87,18 +87,18 @@ def translate_by_vector( return Material(from_ase(atoms)) -@decorator_convert_material_args_kwargs_to_structure -def wrap_to_unit_cell(structure: PymatgenStructure): +def wrap_to_unit_cell(material: Material) -> Material: """ - Wrap atoms to the cell + Wrap the material to the unit cell. Args: - structure (PymatgenStructure): The pymatgen PymatgenStructure object to normalize. + material (Material): The material to wrap. Returns: - PymatgenStructure: The wrapped pymatgen PymatgenStructure object. + Material: The wrapped material. """ - structure.make_supercell((1, 1, 1), to_unit_cell=True) - return structure + atoms = to_ase(material) + atoms.wrap() + return Material(from_ase(atoms)) def filter_material_by_ids(material: Material, ids: List[int], invert: bool = False) -> Material: @@ -437,17 +437,3 @@ def rotate_material(material: Material, axis: List[int], angle: float) -> Materi atoms.wrap() return Material(from_ase(atoms)) - - -def wrap_material(material: Material) -> Material: - """ - Wrap the material to the unit cell. - - Args: - material (Material): The material to wrap. - Returns: - Material: The wrapped material. - """ - atoms = to_ase(material) - atoms.wrap() - return Material(from_ase(atoms)) From e8a22a15ddf3e58312a183d4c2175071ce767578 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 6 Aug 2024 19:13:49 -0700 Subject: [PATCH 48/57] update: change folders and import --- .../made/tools/build/defect/builders.py | 3 +- .../made/tools/build/defect/configuration.py | 2 +- .../made/tools/build/perturbation/builders.py | 3 +- .../tools/build/perturbation/configuration.py | 2 +- src/py/mat3ra/made/tools/utils/__init__.py | 118 +----------------- src/py/mat3ra/made/tools/utils/coordinate.py | 73 ++++++++++- .../mat3ra/made/tools/utils/perturbation.py | 45 +++++++ tests/py/unit/test_tools_build_defect.py | 2 +- .../py/unit/test_tools_build_perturbation.py | 2 +- 9 files changed, 125 insertions(+), 125 deletions(-) create mode 100644 src/py/mat3ra/made/tools/utils/perturbation.py diff --git a/src/py/mat3ra/made/tools/build/defect/builders.py b/src/py/mat3ra/made/tools/build/defect/builders.py index 4346eca4..ab78140a 100644 --- a/src/py/mat3ra/made/tools/build/defect/builders.py +++ b/src/py/mat3ra/made/tools/build/defect/builders.py @@ -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.coordinate import CoordinateConditionBuilder from ..utils import merge_materials from ..slab import SlabConfiguration, create_slab, Termination from ..supercell import create_supercell diff --git a/src/py/mat3ra/made/tools/build/defect/configuration.py b/src/py/mat3ra/made/tools/build/defect/configuration.py index dcd33e33..1beb4305 100644 --- a/src/py/mat3ra/made/tools/build/defect/configuration.py +++ b/src/py/mat3ra/made/tools/build/defect/configuration.py @@ -5,7 +5,7 @@ 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 CoordinateConditionBuilder from .enums import PointDefectTypeEnum, SlabDefectTypeEnum, AtomPlacementMethodEnum, ComplexDefectTypeEnum diff --git a/src/py/mat3ra/made/tools/build/perturbation/builders.py b/src/py/mat3ra/made/tools/build/perturbation/builders.py index e219ab6d..c9702069 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/builders.py +++ b/src/py/mat3ra/made/tools/build/perturbation/builders.py @@ -3,9 +3,8 @@ from mat3ra.made.material import Material from mat3ra.made.tools.build import BaseBuilder -from .configuration import PerturbationConfiguration +from .configuration import PerturbationConfiguration, PerturbationFunctionHolder from ...modify import wrap_to_unit_cell, translate_to_z_level -from ...utils import PerturbationFunctionHolder class PerturbationBuilder(BaseBuilder): diff --git a/src/py/mat3ra/made/tools/build/perturbation/configuration.py b/src/py/mat3ra/made/tools/build/perturbation/configuration.py index bf89bfa5..f81aac9e 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/configuration.py +++ b/src/py/mat3ra/made/tools/build/perturbation/configuration.py @@ -4,7 +4,7 @@ from mat3ra.made.material import Material from pydantic import BaseModel -from ...utils import PerturbationFunctionHolder +from ...utils.perturbation import PerturbationFunctionHolder class PerturbationConfiguration(BaseModel, InMemoryEntity): diff --git a/src/py/mat3ra/made/tools/utils/__init__.py b/src/py/mat3ra/made/tools/utils/__init__.py index 6923da58..acd1281d 100644 --- a/src/py/mat3ra/made/tools/utils/__init__.py +++ b/src/py/mat3ra/made/tools/utils/__init__.py @@ -1,5 +1,5 @@ from functools import wraps -from typing import Callable, Dict, List, Literal, Optional, Tuple +from typing import Callable, List, Optional import numpy as np from mat3ra.utils.matrix import convert_2x2_to_3x3 @@ -75,9 +75,6 @@ def get_norm(vector: List[float]) -> float: return float(np.linalg.norm(vector)) -# Condition functions: - - def transform_coordinate_to_supercell( coordinate: List[float], scaling_factor: Optional[List[int]] = None, @@ -110,116 +107,3 @@ def transform_coordinate_to_supercell( if reverse: converted_array = (np_coordinate - np_translation_vector) * np_scaling_factor return converted_array.tolist() - - -class CoordinateConditionBuilder: - @staticmethod - def create_condition(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 - - @staticmethod - def cylinder(center_position=None, radius: float = 0.25, min_z: float = 0, max_z: float = 1): - if center_position is None: - center_position = [0.5, 0.5] - return CoordinateConditionBuilder.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, - ) - - @staticmethod - def sphere(center_position=None, radius: float = 0.25): - if center_position is None: - center_position = [0.5, 0.5, 0.5] - return CoordinateConditionBuilder.create_condition( - condition_type="sphere", - evaluation_func=is_coordinate_in_sphere, - center_position=center_position, - radius=radius, - ) - - @staticmethod - def triangular_prism( - position_on_surface_1: List[float] = [0, 0], - position_on_surface_2: List[float] = [1, 0], - position_on_surface_3: List[float] = [0, 1], - min_z: float = 0, - max_z: float = 1, - ): - return CoordinateConditionBuilder.create_condition( - condition_type="prism", - evaluation_func=is_coordinate_in_triangular_prism, - coordinate_1=position_on_surface_1, - coordinate_2=position_on_surface_2, - coordinate_3=position_on_surface_3, - min_z=min_z, - max_z=max_z, - ) - - @staticmethod - def box(min_coordinate=None, max_coordinate=None): - if max_coordinate is None: - max_coordinate = [1, 1, 1] - if min_coordinate is None: - min_coordinate = [0, 0, 0] - return CoordinateConditionBuilder.create_condition( - condition_type="box", - evaluation_func=is_coordinate_in_box, - min_coordinate=min_coordinate, - max_coordinate=max_coordinate, - ) - - @staticmethod - def plane(plane_normal: List[float], plane_point_coordinate: List[float]): - return CoordinateConditionBuilder.create_condition( - condition_type="plane", - evaluation_func=is_coordinate_behind_plane, - plane_normal=plane_normal, - plane_point_coordinate=plane_point_coordinate, - ) - - -class PerturbationFunctionHolder: - @staticmethod - def get_coord_transformation(perturbation_json: dict) -> Callable: - new_perturbation_json = perturbation_json.copy() - name = new_perturbation_json.pop("type") - helper_function = PerturbationFunctionHelperFactory.get_class_by_name(name) - # TODO: add type of SineWave (or corresponding one to the return of the factory) - return helper_function.get_transform_coordinates(**new_perturbation_json) - - @staticmethod - def sine_wave( - amplitude: float = 0.1, - wavelength: float = 1, - phase: float = 0, - axis: Optional[Literal["x", "y"]] = "x", - ) -> Tuple[Callable[[List[float]], List[float]], Dict]: - """ - Deform a coordinate using a sine wave. - Args: - amplitude (float): The amplitude of the sine wave in cartesian coordinates. - wavelength (float): The wavelength of the sine wave in cartesian coordinates. - phase (float): The phase of the sine wave in cartesian coordinates. - axis (str): The axis of the direction of the sine wave. - - Returns: - Tuple[Callable[[List[float]], List[float]], Dict]: The perturbation function and its configuration - """ - if axis in AXIS_TO_INDEX_MAP: - index = AXIS_TO_INDEX_MAP[axis] - perturbation_function = PerturbationFunctionHelperFactory.get_class_by_name("sine_wave") - - def perturbation(coordinate: List[float]): - return [ - coordinate[0], - coordinate[1], - coordinate[2] + perturbation_function.get_function(coordinate[index], amplitude, wavelength, phase), - ] - - config = perturbation_function.get_json(amplitude, wavelength, phase, axis) - return perturbation, config diff --git a/src/py/mat3ra/made/tools/utils/coordinate.py b/src/py/mat3ra/made/tools/utils/coordinate.py index e0baee19..fb8e6b7f 100644 --- a/src/py/mat3ra/made/tools/utils/coordinate.py +++ b/src/py/mat3ra/made/tools/utils/coordinate.py @@ -1,5 +1,5 @@ # Place all functions acting on coordinates -from typing import List +from typing import List, Callable, Tuple, Dict import numpy as np @@ -152,3 +152,74 @@ def is_coordinate_behind_plane( np_plane_normal = np.array(plane_normal) np_plane_point = np.array(plane_point_coordinate) return np.dot(np_plane_normal, np_coordinate - np_plane_point) < 0 + + +class CoordinateConditionBuilder: + @staticmethod + def create_condition(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 + + @staticmethod + def cylinder(center_position=None, radius: float = 0.25, min_z: float = 0, max_z: float = 1): + if center_position is None: + center_position = [0.5, 0.5] + return CoordinateConditionBuilder.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, + ) + + @staticmethod + def sphere(center_position=None, radius: float = 0.25): + if center_position is None: + center_position = [0.5, 0.5, 0.5] + return CoordinateConditionBuilder.create_condition( + condition_type="sphere", + evaluation_func=is_coordinate_in_sphere, + center_position=center_position, + radius=radius, + ) + + @staticmethod + def triangular_prism( + position_on_surface_1: List[float] = [0, 0], + position_on_surface_2: List[float] = [1, 0], + position_on_surface_3: List[float] = [0, 1], + min_z: float = 0, + max_z: float = 1, + ): + return CoordinateConditionBuilder.create_condition( + condition_type="prism", + evaluation_func=is_coordinate_in_triangular_prism, + coordinate_1=position_on_surface_1, + coordinate_2=position_on_surface_2, + coordinate_3=position_on_surface_3, + min_z=min_z, + max_z=max_z, + ) + + @staticmethod + def box(min_coordinate=None, max_coordinate=None): + if max_coordinate is None: + max_coordinate = [1, 1, 1] + if min_coordinate is None: + min_coordinate = [0, 0, 0] + return CoordinateConditionBuilder.create_condition( + condition_type="box", + evaluation_func=is_coordinate_in_box, + min_coordinate=min_coordinate, + max_coordinate=max_coordinate, + ) + + @staticmethod + def plane(plane_normal: List[float], plane_point_coordinate: List[float]): + return CoordinateConditionBuilder.create_condition( + condition_type="plane", + evaluation_func=is_coordinate_behind_plane, + plane_normal=plane_normal, + plane_point_coordinate=plane_point_coordinate, + ) diff --git a/src/py/mat3ra/made/tools/utils/perturbation.py b/src/py/mat3ra/made/tools/utils/perturbation.py new file mode 100644 index 00000000..3e1329fc --- /dev/null +++ b/src/py/mat3ra/made/tools/utils/perturbation.py @@ -0,0 +1,45 @@ +from .factories import PerturbationFunctionHelperFactory +from .helpers import AXIS_TO_INDEX_MAP +from typing import List, Tuple, Dict, Callable, Optional, Literal + + +class PerturbationFunctionHolder: + @staticmethod + def get_coord_transformation(perturbation_json: dict) -> Callable: + new_perturbation_json = perturbation_json.copy() + name = new_perturbation_json.pop("type") + helper_function = PerturbationFunctionHelperFactory.get_class_by_name(name) + # TODO: add type of SineWave (or corresponding one to the return of the factory) + return helper_function.get_transform_coordinates(**new_perturbation_json) + + @staticmethod + def sine_wave( + amplitude: float = 0.1, + wavelength: float = 1, + phase: float = 0, + axis: Optional[Literal["x", "y"]] = "x", + ) -> Tuple[Callable[[List[float]], List[float]], Dict]: + """ + Deform a coordinate using a sine wave. + Args: + amplitude (float): The amplitude of the sine wave in cartesian coordinates. + wavelength (float): The wavelength of the sine wave in cartesian coordinates. + phase (float): The phase of the sine wave in cartesian coordinates. + axis (str): The axis of the direction of the sine wave. + + Returns: + Tuple[Callable[[List[float]], List[float]], Dict]: The perturbation function and its configuration + """ + if axis in AXIS_TO_INDEX_MAP: + index = AXIS_TO_INDEX_MAP[axis] + perturbation_function = PerturbationFunctionHelperFactory.get_class_by_name("sine_wave") + + def perturbation(coordinate: List[float]): + return [ + coordinate[0], + coordinate[1], + coordinate[2] + perturbation_function.get_function(coordinate[index], amplitude, wavelength, phase), + ] + + config = perturbation_function.get_json(amplitude, wavelength, phase, axis) + return perturbation, config diff --git a/tests/py/unit/test_tools_build_defect.py b/tests/py/unit/test_tools_build_defect.py index 647610b5..2f4bbca3 100644 --- a/tests/py/unit/test_tools_build_defect.py +++ b/tests/py/unit/test_tools_build_defect.py @@ -19,7 +19,7 @@ PointDefectPairConfiguration, TerraceSlabDefectConfiguration, ) -from mat3ra.made.tools.utils import CoordinateConditionBuilder +from mat3ra.made.tools.utils.coordinate import CoordinateConditionBuilder from mat3ra.utils import assertion as assertion_utils from .fixtures import SLAB_001, SLAB_111 diff --git a/tests/py/unit/test_tools_build_perturbation.py b/tests/py/unit/test_tools_build_perturbation.py index 69cef4e5..4c2891af 100644 --- a/tests/py/unit/test_tools_build_perturbation.py +++ b/tests/py/unit/test_tools_build_perturbation.py @@ -4,7 +4,7 @@ from mat3ra.made.tools.build.perturbation.builders import SlabPerturbationBuilder from mat3ra.made.tools.build.perturbation.configuration import PerturbationConfiguration from mat3ra.made.tools.build.supercell import create_supercell -from mat3ra.made.tools.utils import PerturbationFunctionHolder +from mat3ra.made.tools.utils.perturbation import PerturbationFunctionHolder from mat3ra.utils import assertion as assertion_utils from .fixtures import GRAPHENE From 98a43580321f09b4f39a2cd327fe5cfc63081d36 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 6 Aug 2024 19:19:21 -0700 Subject: [PATCH 49/57] update: renames --- src/py/mat3ra/made/tools/utils/__init__.py | 4 ++-- src/py/mat3ra/made/tools/utils/factories.py | 4 ++-- .../mat3ra/made/tools/utils/{helpers.py => functions.py} | 6 +++--- src/py/mat3ra/made/tools/utils/perturbation.py | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) rename src/py/mat3ra/made/tools/utils/{helpers.py => functions.py} (93%) diff --git a/src/py/mat3ra/made/tools/utils/__init__.py b/src/py/mat3ra/made/tools/utils/__init__.py index acd1281d..6eb87397 100644 --- a/src/py/mat3ra/made/tools/utils/__init__.py +++ b/src/py/mat3ra/made/tools/utils/__init__.py @@ -12,8 +12,8 @@ is_coordinate_in_sphere, is_coordinate_in_triangular_prism, ) -from .factories import PerturbationFunctionHelperFactory -from .helpers import AXIS_TO_INDEX_MAP +from .factories import PerturbationFunctionHolderFactory +from .functions import AXIS_TO_INDEX_MAP DEFAULT_SCALING_FACTOR = np.array([3, 3, 3]) DEFAULT_TRANSLATION_VECTOR = 1 / DEFAULT_SCALING_FACTOR diff --git a/src/py/mat3ra/made/tools/utils/factories.py b/src/py/mat3ra/made/tools/utils/factories.py index 4b74f5cd..22b6343f 100644 --- a/src/py/mat3ra/made/tools/utils/factories.py +++ b/src/py/mat3ra/made/tools/utils/factories.py @@ -1,7 +1,7 @@ from mat3ra.utils.factory import BaseFactory -class PerturbationFunctionHelperFactory(BaseFactory): +class PerturbationFunctionHolderFactory(BaseFactory): __class_registry__ = { - "sine_wave": "mat3ra.made.tools.utils.helpers.SineWave", + "sine_wave": "mat3ra.made.tools.utils.functions.SineWaveFunctionHolder", } diff --git a/src/py/mat3ra/made/tools/utils/helpers.py b/src/py/mat3ra/made/tools/utils/functions.py similarity index 93% rename from src/py/mat3ra/made/tools/utils/helpers.py rename to src/py/mat3ra/made/tools/utils/functions.py index 0fc3675a..8be3ea6d 100644 --- a/src/py/mat3ra/made/tools/utils/helpers.py +++ b/src/py/mat3ra/made/tools/utils/functions.py @@ -50,7 +50,7 @@ def get_json(*args, **kwargs): raise NotImplementedError -class SineWave(FunctionHolder): +class SineWaveFunctionHolder(FunctionHolder): @staticmethod def get_function( w: float, amplitude: float, wavelength: float, phase: float @@ -64,7 +64,7 @@ def get_derivative(w: float, amplitude: float, wavelength: float, phase: float) @staticmethod def get_arc_length_equation(w_prime: float, w: float, amplitude: float, wavelength: float, phase: float) -> float: arc_length = quad( - lambda t: np.sqrt(1 + (SineWave.get_derivative(t, amplitude, wavelength, phase)) ** 2), + lambda t: np.sqrt(1 + (SineWaveFunctionHolder.get_derivative(t, amplitude, wavelength, phase)) ** 2), a=0, b=w_prime, )[0] @@ -80,7 +80,7 @@ def coordinate_transformation(coordinate: List[float]): w = coordinate[index] # Find x' such that the integral from 0 to x' equals x result = root_scalar( - SineWave.get_arc_length_equation, + SineWaveFunctionHolder.get_arc_length_equation, args=(w, amplitude, wavelength, phase), bracket=[0, EQUATION_RANGE_COEFFICIENT * w], method="brentq", diff --git a/src/py/mat3ra/made/tools/utils/perturbation.py b/src/py/mat3ra/made/tools/utils/perturbation.py index 3e1329fc..13ff8485 100644 --- a/src/py/mat3ra/made/tools/utils/perturbation.py +++ b/src/py/mat3ra/made/tools/utils/perturbation.py @@ -1,5 +1,5 @@ -from .factories import PerturbationFunctionHelperFactory -from .helpers import AXIS_TO_INDEX_MAP +from .factories import PerturbationFunctionHolderFactory +from .functions import AXIS_TO_INDEX_MAP from typing import List, Tuple, Dict, Callable, Optional, Literal @@ -8,7 +8,7 @@ class PerturbationFunctionHolder: def get_coord_transformation(perturbation_json: dict) -> Callable: new_perturbation_json = perturbation_json.copy() name = new_perturbation_json.pop("type") - helper_function = PerturbationFunctionHelperFactory.get_class_by_name(name) + helper_function = PerturbationFunctionHolderFactory.get_class_by_name(name) # TODO: add type of SineWave (or corresponding one to the return of the factory) return helper_function.get_transform_coordinates(**new_perturbation_json) @@ -32,7 +32,7 @@ def sine_wave( """ if axis in AXIS_TO_INDEX_MAP: index = AXIS_TO_INDEX_MAP[axis] - perturbation_function = PerturbationFunctionHelperFactory.get_class_by_name("sine_wave") + perturbation_function = PerturbationFunctionHolderFactory.get_class_by_name("sine_wave") def perturbation(coordinate: List[float]): return [ From 3697be421c16a55da55ea9b20fb264052dcaffde Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 6 Aug 2024 19:21:55 -0700 Subject: [PATCH 50/57] chore: run lint fix --- src/py/mat3ra/made/basis.py | 2 +- src/py/mat3ra/made/tools/modify.py | 4 ++-- src/py/mat3ra/made/tools/utils/coordinate.py | 2 +- src/py/mat3ra/made/tools/utils/perturbation.py | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/py/mat3ra/made/basis.py b/src/py/mat3ra/made/basis.py index b86dbd71..da34c13c 100644 --- a/src/py/mat3ra/made/basis.py +++ b/src/py/mat3ra/made/basis.py @@ -24,7 +24,7 @@ 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( diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index bbeb8064..daefdd6b 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -7,8 +7,8 @@ get_atom_indices_within_radius_pbc, get_atomic_coordinates_extremum, ) -from .convert import decorator_convert_material_args_kwargs_to_structure, from_ase, to_ase -from .third_party import PymatgenStructure, ase_add_vacuum +from .convert import from_ase, to_ase +from .third_party import ase_add_vacuum from .utils.coordinate import ( is_coordinate_in_box, is_coordinate_in_cylinder, diff --git a/src/py/mat3ra/made/tools/utils/coordinate.py b/src/py/mat3ra/made/tools/utils/coordinate.py index fb8e6b7f..aa6e2da8 100644 --- a/src/py/mat3ra/made/tools/utils/coordinate.py +++ b/src/py/mat3ra/made/tools/utils/coordinate.py @@ -1,5 +1,5 @@ # Place all functions acting on coordinates -from typing import List, Callable, Tuple, Dict +from typing import Callable, Dict, List, Tuple import numpy as np diff --git a/src/py/mat3ra/made/tools/utils/perturbation.py b/src/py/mat3ra/made/tools/utils/perturbation.py index 13ff8485..169e5227 100644 --- a/src/py/mat3ra/made/tools/utils/perturbation.py +++ b/src/py/mat3ra/made/tools/utils/perturbation.py @@ -1,6 +1,7 @@ +from typing import Callable, Dict, List, Literal, Optional, Tuple + from .factories import PerturbationFunctionHolderFactory from .functions import AXIS_TO_INDEX_MAP -from typing import List, Tuple, Dict, Callable, Optional, Literal class PerturbationFunctionHolder: From a42ebbd6220a57b04686b291a9fba6bef66adb6c Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 6 Aug 2024 21:09:17 -0700 Subject: [PATCH 51/57] update: optimize ann OOP --- .../made/tools/build/perturbation/__init__.py | 1 - .../made/tools/build/perturbation/builders.py | 27 +++--- .../tools/build/perturbation/configuration.py | 8 +- src/py/mat3ra/made/tools/utils/functions.py | 90 +++++++++---------- .../py/unit/test_tools_build_perturbation.py | 16 ++-- 5 files changed, 71 insertions(+), 71 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/perturbation/__init__.py b/src/py/mat3ra/made/tools/build/perturbation/__init__.py index 3f9df2e4..4e799c31 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/__init__.py +++ b/src/py/mat3ra/made/tools/build/perturbation/__init__.py @@ -7,7 +7,6 @@ CellMatchingDistancePreservingSlabPerturbationBuilder, ) from .configuration import PerturbationConfiguration -from .builders import PerturbationFunctionHolder # type: ignore def create_perturbation( diff --git a/src/py/mat3ra/made/tools/build/perturbation/builders.py b/src/py/mat3ra/made/tools/build/perturbation/builders.py index c9702069..a021ced2 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/builders.py +++ b/src/py/mat3ra/made/tools/build/perturbation/builders.py @@ -3,7 +3,7 @@ from mat3ra.made.material import Material from mat3ra.made.tools.build import BaseBuilder -from .configuration import PerturbationConfiguration, PerturbationFunctionHolder +from .configuration import PerturbationConfiguration from ...modify import wrap_to_unit_cell, translate_to_z_level @@ -41,7 +41,7 @@ def _post_process( return [wrap_to_unit_cell(item) for item in items] def _update_material_name(self, material: Material, configuration: _ConfigurationType) -> Material: - perturbation_details = f"Perturbation: {configuration.perturbation_function[1].get('type')}" + perturbation_details = f"Perturbation: {configuration.perturbation_function_holder.get_json().get('type')}" material.name = f"{material.name} ({perturbation_details})" return material @@ -49,8 +49,10 @@ def _update_material_name(self, material: Material, configuration: _Configuratio class SlabPerturbationBuilder(PerturbationBuilder): def create_perturbed_slab(self, configuration: PerturbationConfiguration) -> Material: new_material = self._prepare_material(configuration) - perturbation_function, _ = configuration.perturbation_function - new_coordinates = [perturbation_function(coord) for coord in new_material.basis.coordinates.values] + new_coordinates = [ + [coord[0], coord[1], coord[2] + configuration.perturbation_function_holder.apply_function(coord)] + for coord in new_material.basis.coordinates.values + ] new_material = self._set_new_coordinates(new_material, new_coordinates) return new_material @@ -58,22 +60,27 @@ def create_perturbed_slab(self, configuration: PerturbationConfiguration) -> Mat class DistancePreservingSlabPerturbationBuilder(SlabPerturbationBuilder): def create_perturbed_slab(self, configuration: PerturbationConfiguration) -> Material: new_material = self._prepare_material(configuration) - perturbation_function, perturbation_json = configuration.perturbation_function - coord_transformation_function = PerturbationFunctionHolder.get_coord_transformation(perturbation_json) new_coordinates = [ - perturbation_function(coord_transformation_function(coord)) + configuration.perturbation_function_holder.transform_coordinates(coord) for coord in new_material.basis.coordinates.values ] + new_coordinates = [ + [coord[0], coord[1], coord[2] + configuration.perturbation_function_holder.apply_function(coord)] + for coord in new_coordinates + ] new_material = self._set_new_coordinates(new_material, new_coordinates) return new_material class CellMatchingDistancePreservingSlabPerturbationBuilder(DistancePreservingSlabPerturbationBuilder): def _transform_lattice_vectors(self, configuration: PerturbationConfiguration) -> List[List[float]]: - perturbation_function, perturbation_json = configuration.perturbation_function - coord_transformation_function = PerturbationFunctionHolder.get_coord_transformation(perturbation_json) cell_vectors = configuration.material.basis.cell.vectors_as_array - return [perturbation_function(coord_transformation_function(coord)) for coord in cell_vectors] + return [ + configuration.perturbation_function_holder.transform_coordinates( + configuration.perturbation_function_holder.transform_coordinates(coord) + ) + for coord in cell_vectors + ] def create_perturbed_slab(self, configuration: PerturbationConfiguration) -> Material: new_material = super().create_perturbed_slab(configuration) diff --git a/src/py/mat3ra/made/tools/build/perturbation/configuration.py b/src/py/mat3ra/made/tools/build/perturbation/configuration.py index f81aac9e..dbd96e54 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/configuration.py +++ b/src/py/mat3ra/made/tools/build/perturbation/configuration.py @@ -1,15 +1,13 @@ -from typing import Callable, Dict, Tuple - from mat3ra.code.entity import InMemoryEntity from mat3ra.made.material import Material from pydantic import BaseModel -from ...utils.perturbation import PerturbationFunctionHolder +from ...utils.functions import SineWaveFunctionHolder class PerturbationConfiguration(BaseModel, InMemoryEntity): material: Material - perturbation_function: Tuple[Callable, Dict] = PerturbationFunctionHolder.sine_wave() + perturbation_function_holder: SineWaveFunctionHolder = SineWaveFunctionHolder() use_cartesian_coordinates: bool = True class Config: @@ -17,7 +15,7 @@ class Config: @property def _json(self): - _, perturbation_function_json = self.perturbation_function + perturbation_function_json = self.perturbation_function_holder.get_json() return { "type": self.get_cls_name(), "material": self.material.to_json(), diff --git a/src/py/mat3ra/made/tools/utils/functions.py b/src/py/mat3ra/made/tools/utils/functions.py index 8be3ea6d..5e493bce 100644 --- a/src/py/mat3ra/made/tools/utils/functions.py +++ b/src/py/mat3ra/made/tools/utils/functions.py @@ -1,4 +1,4 @@ -from typing import Callable, List, Literal +from typing import List, Literal import numpy as np from pydantic import BaseModel @@ -10,29 +10,25 @@ class FunctionHolder(BaseModel): - @staticmethod - def get_function(*args, **kwargs): + def apply_function(self, coordinate: List[float]) -> float: """ Get the function of the perturbation. """ raise NotImplementedError - @staticmethod - def get_derivative(*args, **kwargs): + def apply_derivative(self, coordinate: List[float]) -> float: """ Get the derivative of the perturbation function. """ raise NotImplementedError - @staticmethod - def get_arc_length_equation(*args, **kwargs): + def get_arc_length_equation(self, *args, **kwargs) -> float: """ Get the equation to calculate the arc length between [0,0,0] and a given coordinate of the perturbation. """ raise NotImplementedError - @staticmethod - def get_transform_coordinates(*args, **kwargs): + def transform_coordinates(self, coordinate: List[float]) -> List[float]: """ Transform coordinates to preserve the distance between points on a sine wave when perturbation is applied. Achieved by calculating the integral of the length between [0,0,0] and given coordinate. @@ -42,8 +38,7 @@ def get_transform_coordinates(*args, **kwargs): """ raise NotImplementedError - @staticmethod - def get_json(*args, **kwargs): + def get_json(self) -> dict: """ Get the json representation of the perturbation. """ @@ -51,45 +46,46 @@ def get_json(*args, **kwargs): class SineWaveFunctionHolder(FunctionHolder): - @staticmethod - def get_function( - w: float, amplitude: float, wavelength: float, phase: float - ) -> Callable[[List[float]], List[float]]: - return amplitude * np.sin(2 * np.pi * w / wavelength + phase) - - @staticmethod - def get_derivative(w: float, amplitude: float, wavelength: float, phase: float) -> float: - return amplitude * 2 * np.pi / wavelength * np.cos(2 * np.pi * w / wavelength + phase) - - @staticmethod - def get_arc_length_equation(w_prime: float, w: float, amplitude: float, wavelength: float, phase: float) -> float: + amplitude: float = 0.05 + wavelength: float = 1 + phase: float = 0 + axis: Literal["x", "y"] = "x" + + def apply_function(self, coordinate: List[float]) -> float: + w = coordinate[AXIS_TO_INDEX_MAP[self.axis]] + return self.amplitude * np.sin(2 * np.pi * w / self.wavelength + self.phase) + + def apply_derivative(self, coordinate: List[float]) -> float: + w = coordinate[AXIS_TO_INDEX_MAP[self.axis]] + return self.amplitude * 2 * np.pi / self.wavelength * np.cos(2 * np.pi * w / self.wavelength + self.phase) + + def get_arc_length_equation(self, w_prime: float, w: float) -> float: arc_length = quad( - lambda t: np.sqrt(1 + (SineWaveFunctionHolder.get_derivative(t, amplitude, wavelength, phase)) ** 2), + lambda t: np.sqrt(1 + (self.apply_derivative([t]) ** 2)), a=0, b=w_prime, )[0] return arc_length - w - @staticmethod - def get_transform_coordinates( - amplitude: float, wavelength: float, phase: float, axis: Literal["x", "y"] - ) -> Callable[[List[float]], List[float]]: - index = AXIS_TO_INDEX_MAP[axis] - - def coordinate_transformation(coordinate: List[float]): - w = coordinate[index] - # Find x' such that the integral from 0 to x' equals x - result = root_scalar( - SineWaveFunctionHolder.get_arc_length_equation, - args=(w, amplitude, wavelength, phase), - bracket=[0, EQUATION_RANGE_COEFFICIENT * w], - method="brentq", - ) - coordinate[index] = result.root - return coordinate - - return coordinate_transformation - - @staticmethod - def get_json(amplitude: float, wavelength: float, phase: float, axis: Literal["x", "y"]) -> dict: - return {"type": "sine_wave", "amplitude": amplitude, "wavelength": wavelength, "phase": phase, "axis": axis} + def transform_coordinates(self, coordinate: List[float]) -> List[float]: + index = AXIS_TO_INDEX_MAP[self.axis] + + w = coordinate[index] + # Find x' such that the integral from 0 to x' equals x + result = root_scalar( + self.get_arc_length_equation, + args=w, + bracket=[0, EQUATION_RANGE_COEFFICIENT * w], + method="brentq", + ) + coordinate[index] = result.root + return coordinate + + def get_json(self) -> dict: + return { + "type": self.__class__.__name__, + "amplitude": self.amplitude, + "wavelength": self.wavelength, + "phase": self.phase, + "axis": self.axis, + } diff --git a/tests/py/unit/test_tools_build_perturbation.py b/tests/py/unit/test_tools_build_perturbation.py index 4c2891af..45c9ebc2 100644 --- a/tests/py/unit/test_tools_build_perturbation.py +++ b/tests/py/unit/test_tools_build_perturbation.py @@ -4,7 +4,7 @@ from mat3ra.made.tools.build.perturbation.builders import SlabPerturbationBuilder from mat3ra.made.tools.build.perturbation.configuration import PerturbationConfiguration from mat3ra.made.tools.build.supercell import create_supercell -from mat3ra.made.tools.utils.perturbation import PerturbationFunctionHolder +from mat3ra.made.tools.utils.functions import SineWaveFunctionHolder from mat3ra.utils import assertion as assertion_utils from .fixtures import GRAPHENE @@ -16,7 +16,7 @@ def test_sine_perturbation(): perturbation_config = PerturbationConfiguration( material=slab, - perturbation_function=PerturbationFunctionHolder.sine_wave(amplitude=0.05, wavelength=1), + perturbation_function=SineWaveFunctionHolder(amplitude=0.05, wavelength=1), use_cartesian_coordinates=False, ) builder = SlabPerturbationBuilder() @@ -32,19 +32,19 @@ def test_distance_preserved_sine_perturbation(): perturbation_config = PerturbationConfiguration( material=slab, - perturbation_function=PerturbationFunctionHolder.sine_wave(amplitude=0.05, wavelength=1, phase=0.25, axis="y"), + perturbation_function=SineWaveFunctionHolder(amplitude=0.05, wavelength=1, phase=0.25, axis="y"), use_cartesian_coordinates=False, ) perturbed_slab = create_perturbation(configuration=perturbation_config, preserve_distance=True) # Check selected atoms to avoid using 100+ atoms fixture - assertion_utils.assert_deep_almost_equal([0.0, 0.0, 0.512053493], perturbed_slab.basis.coordinates.values[0]) + assertion_utils.assert_deep_almost_equal([0.0, 0.0, 0.5], perturbed_slab.basis.coordinates.values[0]) assertion_utils.assert_deep_almost_equal( - [0.201165188, 0.099003051, 0.537490872], perturbed_slab.basis.coordinates.values[42] + [0.201132951, 0.1, 0.546942315], perturbed_slab.basis.coordinates.values[42] ) # Value taken from visually inspected notebook expected_cell = Cell( - vector1=[24.67291, 0.0, 0.012370198], - vector2=[-12.336455, 20.864413342, -0.028311143], - vector3=[0.0, 0.0, 20.012370198], + vector1=[23.517094, 0.0, 0.0], + vector2=[-11.758818, 21.367367, 0.0], + vector3=[0.0, 0.0, 20.0], ) assertion_utils.assert_deep_almost_equal(expected_cell.vectors_as_array, perturbed_slab.basis.cell.vectors_as_array) From e8f49476ace23a08ff2423f4d4466959d3e228a0 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Tue, 6 Aug 2024 21:11:07 -0700 Subject: [PATCH 52/57] chore: add a todo memo --- src/py/mat3ra/made/tools/utils/perturbation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/py/mat3ra/made/tools/utils/perturbation.py b/src/py/mat3ra/made/tools/utils/perturbation.py index 169e5227..c70ae51d 100644 --- a/src/py/mat3ra/made/tools/utils/perturbation.py +++ b/src/py/mat3ra/made/tools/utils/perturbation.py @@ -4,6 +4,9 @@ from .functions import AXIS_TO_INDEX_MAP +# TODO: maek use of this class in the perturbation configuration +# use it to either select the preconfigured function from function holders +# or to generate a new one with sympy function definitions class PerturbationFunctionHolder: @staticmethod def get_coord_transformation(perturbation_json: dict) -> Callable: From f84e8735ee04a836f6ac5fe9ede71b7c6de43d32 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 7 Aug 2024 13:21:04 -0700 Subject: [PATCH 53/57] update: optimize with OOP --- .../made/tools/build/perturbation/builders.py | 5 +- .../tools/build/perturbation/configuration.py | 4 +- src/py/mat3ra/made/tools/utils/functions.py | 42 ++++++++++------ .../mat3ra/made/tools/utils/perturbation.py | 49 ------------------- .../py/unit/test_tools_build_perturbation.py | 6 +-- 5 files changed, 35 insertions(+), 71 deletions(-) delete mode 100644 src/py/mat3ra/made/tools/utils/perturbation.py diff --git a/src/py/mat3ra/made/tools/build/perturbation/builders.py b/src/py/mat3ra/made/tools/build/perturbation/builders.py index a021ced2..091364b6 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/builders.py +++ b/src/py/mat3ra/made/tools/build/perturbation/builders.py @@ -50,7 +50,7 @@ class SlabPerturbationBuilder(PerturbationBuilder): def create_perturbed_slab(self, configuration: PerturbationConfiguration) -> Material: new_material = self._prepare_material(configuration) new_coordinates = [ - [coord[0], coord[1], coord[2] + configuration.perturbation_function_holder.apply_function(coord)] + configuration.perturbation_function_holder.apply_perturbation(coord) for coord in new_material.basis.coordinates.values ] new_material = self._set_new_coordinates(new_material, new_coordinates) @@ -65,8 +65,7 @@ def create_perturbed_slab(self, configuration: PerturbationConfiguration) -> Mat for coord in new_material.basis.coordinates.values ] new_coordinates = [ - [coord[0], coord[1], coord[2] + configuration.perturbation_function_holder.apply_function(coord)] - for coord in new_coordinates + configuration.perturbation_function_holder.apply_perturbation(coord) for coord in new_coordinates ] new_material = self._set_new_coordinates(new_material, new_coordinates) return new_material diff --git a/src/py/mat3ra/made/tools/build/perturbation/configuration.py b/src/py/mat3ra/made/tools/build/perturbation/configuration.py index dbd96e54..1b245794 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/configuration.py +++ b/src/py/mat3ra/made/tools/build/perturbation/configuration.py @@ -2,12 +2,12 @@ from mat3ra.made.material import Material from pydantic import BaseModel -from ...utils.functions import SineWaveFunctionHolder +from ...utils.functions import SineWavePerturbationFunctionHolder class PerturbationConfiguration(BaseModel, InMemoryEntity): material: Material - perturbation_function_holder: SineWaveFunctionHolder = SineWaveFunctionHolder() + perturbation_function_holder: SineWavePerturbationFunctionHolder = SineWavePerturbationFunctionHolder() use_cartesian_coordinates: bool = True class Config: diff --git a/src/py/mat3ra/made/tools/utils/functions.py b/src/py/mat3ra/made/tools/utils/functions.py index 5e493bce..a32b026c 100644 --- a/src/py/mat3ra/made/tools/utils/functions.py +++ b/src/py/mat3ra/made/tools/utils/functions.py @@ -12,22 +12,41 @@ class FunctionHolder(BaseModel): def apply_function(self, coordinate: List[float]) -> float: """ - Get the function of the perturbation. + Get the value of the function at the given coordinate. """ raise NotImplementedError def apply_derivative(self, coordinate: List[float]) -> float: """ - Get the derivative of the perturbation function. + Get the derivative of the function at the given coordinate """ raise NotImplementedError - def get_arc_length_equation(self, *args, **kwargs) -> float: + def get_arc_length(self, a: float, b: float) -> float: """ - Get the equation to calculate the arc length between [0,0,0] and a given coordinate of the perturbation. + Get the arc length of the function between a and b. """ raise NotImplementedError + def get_json(self) -> dict: + """ + Get the json representation of the function holder. + """ + raise NotImplementedError + + +class PerturbationFunctionHolder(FunctionHolder): + def get_arc_length_equation(self, w_prime: float, w: float) -> float: + """ + Get the arc length equation for the perturbation function. + """ + arc_length = quad( + lambda t: np.sqrt(1 + (self.apply_derivative([t]) ** 2)), + a=0, + b=w_prime, + )[0] + return arc_length - w + def transform_coordinates(self, coordinate: List[float]) -> List[float]: """ Transform coordinates to preserve the distance between points on a sine wave when perturbation is applied. @@ -38,14 +57,14 @@ def transform_coordinates(self, coordinate: List[float]) -> List[float]: """ raise NotImplementedError - def get_json(self) -> dict: + def apply_perturbation(self, coordinate: List[float]) -> List[float]: """ - Get the json representation of the perturbation. + Apply the perturbation to the given coordinate. """ raise NotImplementedError -class SineWaveFunctionHolder(FunctionHolder): +class SineWavePerturbationFunctionHolder(PerturbationFunctionHolder): amplitude: float = 0.05 wavelength: float = 1 phase: float = 0 @@ -59,13 +78,8 @@ def apply_derivative(self, coordinate: List[float]) -> float: w = coordinate[AXIS_TO_INDEX_MAP[self.axis]] return self.amplitude * 2 * np.pi / self.wavelength * np.cos(2 * np.pi * w / self.wavelength + self.phase) - def get_arc_length_equation(self, w_prime: float, w: float) -> float: - arc_length = quad( - lambda t: np.sqrt(1 + (self.apply_derivative([t]) ** 2)), - a=0, - b=w_prime, - )[0] - return arc_length - w + def apply_perturbation(self, coordinate: List[float]) -> List[float]: + return [coordinate[0], coordinate[1], coordinate[2] + self.apply_function(coordinate)] def transform_coordinates(self, coordinate: List[float]) -> List[float]: index = AXIS_TO_INDEX_MAP[self.axis] diff --git a/src/py/mat3ra/made/tools/utils/perturbation.py b/src/py/mat3ra/made/tools/utils/perturbation.py deleted file mode 100644 index c70ae51d..00000000 --- a/src/py/mat3ra/made/tools/utils/perturbation.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import Callable, Dict, List, Literal, Optional, Tuple - -from .factories import PerturbationFunctionHolderFactory -from .functions import AXIS_TO_INDEX_MAP - - -# TODO: maek use of this class in the perturbation configuration -# use it to either select the preconfigured function from function holders -# or to generate a new one with sympy function definitions -class PerturbationFunctionHolder: - @staticmethod - def get_coord_transformation(perturbation_json: dict) -> Callable: - new_perturbation_json = perturbation_json.copy() - name = new_perturbation_json.pop("type") - helper_function = PerturbationFunctionHolderFactory.get_class_by_name(name) - # TODO: add type of SineWave (or corresponding one to the return of the factory) - return helper_function.get_transform_coordinates(**new_perturbation_json) - - @staticmethod - def sine_wave( - amplitude: float = 0.1, - wavelength: float = 1, - phase: float = 0, - axis: Optional[Literal["x", "y"]] = "x", - ) -> Tuple[Callable[[List[float]], List[float]], Dict]: - """ - Deform a coordinate using a sine wave. - Args: - amplitude (float): The amplitude of the sine wave in cartesian coordinates. - wavelength (float): The wavelength of the sine wave in cartesian coordinates. - phase (float): The phase of the sine wave in cartesian coordinates. - axis (str): The axis of the direction of the sine wave. - - Returns: - Tuple[Callable[[List[float]], List[float]], Dict]: The perturbation function and its configuration - """ - if axis in AXIS_TO_INDEX_MAP: - index = AXIS_TO_INDEX_MAP[axis] - perturbation_function = PerturbationFunctionHolderFactory.get_class_by_name("sine_wave") - - def perturbation(coordinate: List[float]): - return [ - coordinate[0], - coordinate[1], - coordinate[2] + perturbation_function.get_function(coordinate[index], amplitude, wavelength, phase), - ] - - config = perturbation_function.get_json(amplitude, wavelength, phase, axis) - return perturbation, config diff --git a/tests/py/unit/test_tools_build_perturbation.py b/tests/py/unit/test_tools_build_perturbation.py index 45c9ebc2..8767d8f7 100644 --- a/tests/py/unit/test_tools_build_perturbation.py +++ b/tests/py/unit/test_tools_build_perturbation.py @@ -4,7 +4,7 @@ from mat3ra.made.tools.build.perturbation.builders import SlabPerturbationBuilder from mat3ra.made.tools.build.perturbation.configuration import PerturbationConfiguration from mat3ra.made.tools.build.supercell import create_supercell -from mat3ra.made.tools.utils.functions import SineWaveFunctionHolder +from mat3ra.made.tools.utils.functions import SineWavePerturbationFunctionHolder from mat3ra.utils import assertion as assertion_utils from .fixtures import GRAPHENE @@ -16,7 +16,7 @@ def test_sine_perturbation(): perturbation_config = PerturbationConfiguration( material=slab, - perturbation_function=SineWaveFunctionHolder(amplitude=0.05, wavelength=1), + perturbation_function=SineWavePerturbationFunctionHolder(amplitude=0.05, wavelength=1), use_cartesian_coordinates=False, ) builder = SlabPerturbationBuilder() @@ -32,7 +32,7 @@ def test_distance_preserved_sine_perturbation(): perturbation_config = PerturbationConfiguration( material=slab, - perturbation_function=SineWaveFunctionHolder(amplitude=0.05, wavelength=1, phase=0.25, axis="y"), + perturbation_function=SineWavePerturbationFunctionHolder(amplitude=0.05, wavelength=1, phase=0.25, axis="y"), use_cartesian_coordinates=False, ) perturbed_slab = create_perturbation(configuration=perturbation_config, preserve_distance=True) From 963822aa7b266639aad80a8b43313a02a3ba4bae Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:27:35 -0700 Subject: [PATCH 54/57] update: add set coordintes as a method --- src/py/mat3ra/made/material.py | 5 +++++ .../mat3ra/made/tools/build/perturbation/builders.py | 12 ++---------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/py/mat3ra/made/material.py b/src/py/mat3ra/made/material.py index 9b50ba47..1a5d0162 100644 --- a/src/py/mat3ra/made/material.py +++ b/src/py/mat3ra/made/material.py @@ -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 diff --git a/src/py/mat3ra/made/tools/build/perturbation/builders.py b/src/py/mat3ra/made/tools/build/perturbation/builders.py index 091364b6..f13ba6d3 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/builders.py +++ b/src/py/mat3ra/made/tools/build/perturbation/builders.py @@ -20,14 +20,6 @@ def _prepare_material(configuration: _ConfigurationType) -> _GeneratedItemType: new_material.to_cartesian() return new_material - @staticmethod - def _set_new_coordinates(new_material: Material, new_coordinates: List[List[float]]) -> Material: - new_basis = new_material.basis.copy() - new_basis.coordinates.values = new_coordinates - new_basis.to_crystal() - new_material.basis = new_basis - return new_material - def _generate(self, configuration: _ConfigurationType) -> List[_GeneratedItemType]: """Generate materials with applied continuous perturbation based on the given configuration.""" new_material = self.create_perturbed_slab(configuration) @@ -53,7 +45,7 @@ def create_perturbed_slab(self, configuration: PerturbationConfiguration) -> Mat configuration.perturbation_function_holder.apply_perturbation(coord) for coord in new_material.basis.coordinates.values ] - new_material = self._set_new_coordinates(new_material, new_coordinates) + new_material.set_coordinates(new_coordinates) return new_material @@ -67,7 +59,7 @@ def create_perturbed_slab(self, configuration: PerturbationConfiguration) -> Mat new_coordinates = [ configuration.perturbation_function_holder.apply_perturbation(coord) for coord in new_coordinates ] - new_material = self._set_new_coordinates(new_material, new_coordinates) + new_material.set_coordinates(new_coordinates) return new_material From 5f2dcf42829d5e245ee53e71b8f10679e338ef5f Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 11 Aug 2024 15:55:22 -0700 Subject: [PATCH 55/57] update: rearrange --- .../tools/build/perturbation/configuration.py | 2 +- src/py/mat3ra/made/tools/utils/functions.py | 79 +------------------ .../mat3ra/made/tools/utils/perturbation.py | 77 ++++++++++++++++++ .../py/unit/test_tools_build_perturbation.py | 2 +- 4 files changed, 82 insertions(+), 78 deletions(-) create mode 100644 src/py/mat3ra/made/tools/utils/perturbation.py diff --git a/src/py/mat3ra/made/tools/build/perturbation/configuration.py b/src/py/mat3ra/made/tools/build/perturbation/configuration.py index 1b245794..767ffa8a 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/configuration.py +++ b/src/py/mat3ra/made/tools/build/perturbation/configuration.py @@ -2,7 +2,7 @@ from mat3ra.made.material import Material from pydantic import BaseModel -from ...utils.functions import SineWavePerturbationFunctionHolder +from ...utils.perturbation import SineWavePerturbationFunctionHolder class PerturbationConfiguration(BaseModel, InMemoryEntity): diff --git a/src/py/mat3ra/made/tools/utils/functions.py b/src/py/mat3ra/made/tools/utils/functions.py index a32b026c..9d0ec4ea 100644 --- a/src/py/mat3ra/made/tools/utils/functions.py +++ b/src/py/mat3ra/made/tools/utils/functions.py @@ -1,9 +1,6 @@ -from typing import List, Literal +from typing import List -import numpy as np from pydantic import BaseModel -from scipy.integrate import quad -from scipy.optimize import root_scalar AXIS_TO_INDEX_MAP = {"x": 0, "y": 1, "z": 2} EQUATION_RANGE_COEFFICIENT = 5 @@ -16,13 +13,13 @@ def apply_function(self, coordinate: List[float]) -> float: """ raise NotImplementedError - def apply_derivative(self, coordinate: List[float]) -> float: + def calculate_derivative(self, coordinate: List[float]) -> float: """ Get the derivative of the function at the given coordinate """ raise NotImplementedError - def get_arc_length(self, a: float, b: float) -> float: + def calculate_arc_length(self, a: float, b: float) -> float: """ Get the arc length of the function between a and b. """ @@ -33,73 +30,3 @@ def get_json(self) -> dict: Get the json representation of the function holder. """ raise NotImplementedError - - -class PerturbationFunctionHolder(FunctionHolder): - def get_arc_length_equation(self, w_prime: float, w: float) -> float: - """ - Get the arc length equation for the perturbation function. - """ - arc_length = quad( - lambda t: np.sqrt(1 + (self.apply_derivative([t]) ** 2)), - a=0, - b=w_prime, - )[0] - return arc_length - w - - def transform_coordinates(self, coordinate: List[float]) -> List[float]: - """ - Transform coordinates to preserve the distance between points on a sine wave when perturbation is applied. - Achieved by calculating the integral of the length between [0,0,0] and given coordinate. - - Returns: - Callable[[List[float]], List[float]]: The coordinates transformation function. - """ - raise NotImplementedError - - def apply_perturbation(self, coordinate: List[float]) -> List[float]: - """ - Apply the perturbation to the given coordinate. - """ - raise NotImplementedError - - -class SineWavePerturbationFunctionHolder(PerturbationFunctionHolder): - amplitude: float = 0.05 - wavelength: float = 1 - phase: float = 0 - axis: Literal["x", "y"] = "x" - - def apply_function(self, coordinate: List[float]) -> float: - w = coordinate[AXIS_TO_INDEX_MAP[self.axis]] - return self.amplitude * np.sin(2 * np.pi * w / self.wavelength + self.phase) - - def apply_derivative(self, coordinate: List[float]) -> float: - w = coordinate[AXIS_TO_INDEX_MAP[self.axis]] - return self.amplitude * 2 * np.pi / self.wavelength * np.cos(2 * np.pi * w / self.wavelength + self.phase) - - def apply_perturbation(self, coordinate: List[float]) -> List[float]: - return [coordinate[0], coordinate[1], coordinate[2] + self.apply_function(coordinate)] - - def transform_coordinates(self, coordinate: List[float]) -> List[float]: - index = AXIS_TO_INDEX_MAP[self.axis] - - w = coordinate[index] - # Find x' such that the integral from 0 to x' equals x - result = root_scalar( - self.get_arc_length_equation, - args=w, - bracket=[0, EQUATION_RANGE_COEFFICIENT * w], - method="brentq", - ) - coordinate[index] = result.root - return coordinate - - def get_json(self) -> dict: - return { - "type": self.__class__.__name__, - "amplitude": self.amplitude, - "wavelength": self.wavelength, - "phase": self.phase, - "axis": self.axis, - } diff --git a/src/py/mat3ra/made/tools/utils/perturbation.py b/src/py/mat3ra/made/tools/utils/perturbation.py new file mode 100644 index 00000000..8e5ff4cf --- /dev/null +++ b/src/py/mat3ra/made/tools/utils/perturbation.py @@ -0,0 +1,77 @@ +from typing import List, Literal + +import numpy as np +from scipy.integrate import quad +from scipy.optimize import root_scalar + +from .functions import AXIS_TO_INDEX_MAP, EQUATION_RANGE_COEFFICIENT, FunctionHolder + + +class PerturbationFunctionHolder(FunctionHolder): + def calculate_arc_length_equation(self, w_prime: float, w: float) -> float: + """ + Get the arc length equation for the perturbation function. + """ + arc_length = quad( + lambda t: np.sqrt(1 + (self.calculate_derivative([t]) ** 2)), + a=0, + b=w_prime, + )[0] + return arc_length - w + + def transform_coordinates(self, coordinate: List[float]) -> List[float]: + """ + Transform coordinates to preserve the distance between points on a sine wave when perturbation is applied. + Achieved by calculating the integral of the length between [0,0,0] and given coordinate. + + Returns: + Callable[[List[float]], List[float]]: The coordinates transformation function. + """ + raise NotImplementedError + + def apply_perturbation(self, coordinate: List[float]) -> List[float]: + """ + Apply the perturbation to the given coordinate. + """ + raise NotImplementedError + + +class SineWavePerturbationFunctionHolder(PerturbationFunctionHolder): + amplitude: float = 0.05 + wavelength: float = 1 + phase: float = 0 + axis: Literal["x", "y"] = "x" + + def apply_function(self, coordinate: List[float]) -> float: + w = coordinate[AXIS_TO_INDEX_MAP[self.axis]] + return self.amplitude * np.sin(2 * np.pi * w / self.wavelength + self.phase) + + def calculate_derivative(self, coordinate: List[float]) -> float: + w = coordinate[AXIS_TO_INDEX_MAP[self.axis]] + return self.amplitude * 2 * np.pi / self.wavelength * np.cos(2 * np.pi * w / self.wavelength + self.phase) + + def apply_perturbation(self, coordinate: List[float]) -> List[float]: + return [coordinate[0], coordinate[1], coordinate[2] + self.apply_function(coordinate)] + + def transform_coordinates(self, coordinate: List[float]) -> List[float]: + index = AXIS_TO_INDEX_MAP[self.axis] + + w = coordinate[index] + # Find x' such that the integral from 0 to x' equals x + result = root_scalar( + self.calculate_arc_length_equation, + args=w, + bracket=[0, EQUATION_RANGE_COEFFICIENT * w], + method="brentq", + ) + coordinate[index] = result.root + return coordinate + + def get_json(self) -> dict: + return { + "type": self.__class__.__name__, + "amplitude": self.amplitude, + "wavelength": self.wavelength, + "phase": self.phase, + "axis": self.axis, + } diff --git a/tests/py/unit/test_tools_build_perturbation.py b/tests/py/unit/test_tools_build_perturbation.py index 8767d8f7..86232a5d 100644 --- a/tests/py/unit/test_tools_build_perturbation.py +++ b/tests/py/unit/test_tools_build_perturbation.py @@ -4,7 +4,7 @@ from mat3ra.made.tools.build.perturbation.builders import SlabPerturbationBuilder from mat3ra.made.tools.build.perturbation.configuration import PerturbationConfiguration from mat3ra.made.tools.build.supercell import create_supercell -from mat3ra.made.tools.utils.functions import SineWavePerturbationFunctionHolder +from mat3ra.made.tools.utils.perturbation import SineWavePerturbationFunctionHolder from mat3ra.utils import assertion as assertion_utils from .fixtures import GRAPHENE From 8ce0856b613d487b49aee627fa3f43b069ebfa8b Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 11 Aug 2024 17:32:27 -0700 Subject: [PATCH 56/57] update: remove tuples --- .../made/tools/build/defect/builders.py | 8 +- .../made/tools/build/defect/configuration.py | 27 +++- src/py/mat3ra/made/tools/utils/coordinate.py | 120 ++++++++---------- tests/py/unit/test_tools_build_defect.py | 6 +- 4 files changed, 83 insertions(+), 78 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/defect/builders.py b/src/py/mat3ra/made/tools/build/defect/builders.py index ab78140a..a9c25abd 100644 --- a/src/py/mat3ra/made/tools/build/defect/builders.py +++ b/src/py/mat3ra/made/tools/build/defect/builders.py @@ -31,7 +31,7 @@ ) from ....utils import get_center_of_coordinates from ...utils import transform_coordinate_to_supercell -from ...utils.coordinate import CoordinateConditionBuilder +from ...utils import coordinate as CoordinateCondition from ..utils import merge_materials from ..slab import SlabConfiguration, create_slab, Termination from ..supercell import create_supercell @@ -437,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, @@ -593,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, diff --git a/src/py/mat3ra/made/tools/build/defect/configuration.py b/src/py/mat3ra/made/tools/build/defect/configuration.py index 1beb4305..1ae42761 100644 --- a/src/py/mat3ra/made/tools/build/defect/configuration.py +++ b/src/py/mat3ra/made/tools/build/defect/configuration.py @@ -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.coordinate 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): @@ -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.to_json(), } diff --git a/src/py/mat3ra/made/tools/utils/coordinate.py b/src/py/mat3ra/made/tools/utils/coordinate.py index aa6e2da8..eba308e1 100644 --- a/src/py/mat3ra/made/tools/utils/coordinate.py +++ b/src/py/mat3ra/made/tools/utils/coordinate.py @@ -1,7 +1,8 @@ # Place all functions acting on coordinates -from typing import Callable, Dict, List, Tuple +from typing import Dict, List import numpy as np +from pydantic import BaseModel, Field def is_coordinate_in_cylinder( @@ -154,72 +155,61 @@ def is_coordinate_behind_plane( return np.dot(np_plane_normal, np_coordinate - np_plane_point) < 0 -class CoordinateConditionBuilder: - @staticmethod - def create_condition(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 - - @staticmethod - def cylinder(center_position=None, radius: float = 0.25, min_z: float = 0, max_z: float = 1): - if center_position is None: - center_position = [0.5, 0.5] - return CoordinateConditionBuilder.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, - ) +class CoordinateCondition(BaseModel): + def condition(self, coordinate: List[float]) -> bool: + raise NotImplementedError - @staticmethod - def sphere(center_position=None, radius: float = 0.25): - if center_position is None: - center_position = [0.5, 0.5, 0.5] - return CoordinateConditionBuilder.create_condition( - condition_type="sphere", - evaluation_func=is_coordinate_in_sphere, - center_position=center_position, - radius=radius, - ) + def to_json(self) -> Dict: + return self.dict() - @staticmethod - def triangular_prism( - position_on_surface_1: List[float] = [0, 0], - position_on_surface_2: List[float] = [1, 0], - position_on_surface_3: List[float] = [0, 1], - min_z: float = 0, - max_z: float = 1, - ): - return CoordinateConditionBuilder.create_condition( - condition_type="prism", - evaluation_func=is_coordinate_in_triangular_prism, - coordinate_1=position_on_surface_1, - coordinate_2=position_on_surface_2, - coordinate_3=position_on_surface_3, - min_z=min_z, - max_z=max_z, - ) - @staticmethod - def box(min_coordinate=None, max_coordinate=None): - if max_coordinate is None: - max_coordinate = [1, 1, 1] - if min_coordinate is None: - min_coordinate = [0, 0, 0] - return CoordinateConditionBuilder.create_condition( - condition_type="box", - evaluation_func=is_coordinate_in_box, - min_coordinate=min_coordinate, - max_coordinate=max_coordinate, - ) +class CylinderCoordinateCondition(CoordinateCondition): + center_position: List[float] = Field(default_factory=lambda: [0.5, 0.5]) + radius: float = 0.25 + min_z: float = 0 + max_z: float = 1 + + def condition(self, coordinate: List[float]) -> bool: + return is_coordinate_in_cylinder(coordinate, self.center_position, self.radius, self.min_z, self.max_z) + + +class SphereCoordinateCondition(CoordinateCondition): + center_position: List[float] = Field(default_factory=lambda: [0.5, 0.5]) + radius: float = 0.25 + + def condition(self, coordinate: List[float]) -> bool: + return is_coordinate_in_sphere(coordinate, self.center_position, self.radius) - @staticmethod - def plane(plane_normal: List[float], plane_point_coordinate: List[float]): - return CoordinateConditionBuilder.create_condition( - condition_type="plane", - evaluation_func=is_coordinate_behind_plane, - plane_normal=plane_normal, - plane_point_coordinate=plane_point_coordinate, + +class BoxCoordinateCondition(CoordinateCondition): + min_coordinate: List[float] = Field(default_factory=lambda: [0, 0, 0]) + max_coordinate: List[float] = Field(default_factory=lambda: [1, 1, 1]) + + def condition(self, coordinate: List[float]) -> bool: + return is_coordinate_in_box(coordinate, self.min_coordinate, self.max_coordinate) + + +class TriangularPrismCoordinateCondition(CoordinateCondition): + position_on_surface_1: List[float] = [0, 0] + position_on_surface_2: List[float] = [1, 0] + position_on_surface_3: List[float] = [0, 1] + min_z: float = 0 + max_z: float = 1 + + def condition(self, coordinate: List[float]) -> bool: + return is_coordinate_in_triangular_prism( + coordinate, + self.position_on_surface_1, + self.position_on_surface_2, + self.position_on_surface_3, + self.min_z, + self.max_z, ) + + +class PlaneCoordinateCondition(CoordinateCondition): + plane_normal: List[float] + plane_point_coordinate: List[float] + + def condition(self, coordinate: List[float]) -> bool: + return is_coordinate_behind_plane(coordinate, self.plane_normal, self.plane_point_coordinate) diff --git a/tests/py/unit/test_tools_build_defect.py b/tests/py/unit/test_tools_build_defect.py index 2f4bbca3..1ee349fa 100644 --- a/tests/py/unit/test_tools_build_defect.py +++ b/tests/py/unit/test_tools_build_defect.py @@ -19,7 +19,7 @@ PointDefectPairConfiguration, TerraceSlabDefectConfiguration, ) -from mat3ra.made.tools.utils.coordinate import CoordinateConditionBuilder +from mat3ra.made.tools.utils import coordinate as CoordinateCondition from mat3ra.utils import assertion as assertion_utils from .fixtures import SLAB_001, SLAB_111 @@ -114,7 +114,9 @@ def test_create_crystal_site_adatom(): def test_create_island(): - condition = CoordinateConditionBuilder.cylinder(center_position=[0.625, 0.5], radius=0.25, min_z=0, max_z=1) + condition = CoordinateCondition.CylinderCoordinateCondition( + center_position=[0.625, 0.5], radius=0.25, min_z=0, max_z=1 + ) island_config = IslandSlabDefectConfiguration( crystal=SLAB_111, defect_type="island", From 4c6a3919efd96a04da08841408dc3103b6d0af93 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 11 Aug 2024 18:05:17 -0700 Subject: [PATCH 57/57] update: rename --- src/py/mat3ra/made/tools/build/defect/configuration.py | 2 +- src/py/mat3ra/made/tools/utils/coordinate.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/defect/configuration.py b/src/py/mat3ra/made/tools/build/defect/configuration.py index 1ae42761..43e49f25 100644 --- a/src/py/mat3ra/made/tools/build/defect/configuration.py +++ b/src/py/mat3ra/made/tools/build/defect/configuration.py @@ -191,7 +191,7 @@ def _json(self): **super()._json, "type": self.get_cls_name(), "defect_type": self.defect_type.name, - "condition": self.condition.to_json(), + "condition": self.condition.get_json(), } diff --git a/src/py/mat3ra/made/tools/utils/coordinate.py b/src/py/mat3ra/made/tools/utils/coordinate.py index eba308e1..683c54b3 100644 --- a/src/py/mat3ra/made/tools/utils/coordinate.py +++ b/src/py/mat3ra/made/tools/utils/coordinate.py @@ -159,8 +159,10 @@ class CoordinateCondition(BaseModel): def condition(self, coordinate: List[float]) -> bool: raise NotImplementedError - def to_json(self) -> Dict: - return self.dict() + def get_json(self) -> Dict: + json = {"type": self.__class__.__name__} + json.update(self.dict()) + return json class CylinderCoordinateCondition(CoordinateCondition):