From 0372340b1e26931913070b942e2dc9bf3ccaaed8 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Jan 2025 15:21:54 -0800 Subject: [PATCH 01/20] update: add function to create empty material --- src/py/mat3ra/made/material.py | 62 +++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/py/mat3ra/made/material.py b/src/py/mat3ra/made/material.py index 6af5e9a4..a985d472 100644 --- a/src/py/mat3ra/made/material.py +++ b/src/py/mat3ra/made/material.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Union, Optional from mat3ra.code.constants import AtomicCoordinateUnits, Units from mat3ra.code.entity import HasDescriptionHasMetadataNamedDefaultableInMemoryEntity @@ -112,3 +112,63 @@ def add_atom(self, element: str, coordinate: List[float], use_cartesian_coordina new_basis = self.basis.copy() new_basis.add_atom(element, coordinate, use_cartesian_coordinates) self.basis = new_basis + + + @classmethod + def create_empty( + cls, + a: float = 1.0, + b: Optional[float] = None, + c: Optional[float] = None, + alpha: float = 90.0, + beta: float = 90.0, + gamma: float = 90.0, + lattice_type: str = "CUB", + name: str = "New Material" + ) -> "Material": + """ + Create an empty material with specified lattice parameters. + + Args: + a (float): First lattice constant. Defaults to 1.0. + b (float, optional): Second lattice constant. Defaults to value of 'a'. + c (float, optional): Third lattice constant. Defaults to value of 'a'. + alpha (float): First lattice angle in degrees. Defaults to 90.0. + beta (float): Second lattice angle in degrees. Defaults to 90.0. + gamma (float): Third lattice angle in degrees. Defaults to 90.0. + lattice_type (str): Type of lattice. Defaults to "CUB". + name (str): Name of the material. Defaults to "New Material". + + Returns: + Material: A new empty Material instance with specified lattice + """ + b = b if b is not None else a + c = c if c is not None else a + + basis_config = { + "elements": [], + "coordinates": [], + "units": AtomicCoordinateUnits.cartesian, + } + + lattice_config = { + "type": lattice_type, + "a": a, + "b": b, + "c": c, + "alpha": alpha, + "beta": beta, + "gamma": gamma, + "units": { + "length": Units.angstrom, + "angle": Units.degree, + } + } + + config = { + "name": name, + "basis": basis_config, + "lattice": lattice_config + } + + return cls(config) From c60c67447417e4e14e7c3a6f774b483c5c9d8cd6 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Jan 2025 15:22:06 -0800 Subject: [PATCH 02/20] update: add tests for empty material --- tests/py/unit/test_material.py | 45 ++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/py/unit/test_material.py b/tests/py/unit/test_material.py index 359bdab9..a072494c 100644 --- a/tests/py/unit/test_material.py +++ b/tests/py/unit/test_material.py @@ -26,3 +26,48 @@ def test_basis_to_json(): # TODO: Add test to check if basis.cell is changed when lattice of material is changed, and vice versa + + +def test_create_empty(): + """Test creating empty materials with different lattice parameters""" + # Test default parameters + material = Material.create_empty() + assert material.basis.elements.values == [] + assert material.basis.coordinates.values == [] + assert material.lattice.type == "CUB" + assert material.lattice.a == 1.0 + assert material.lattice.b == 1.0 + assert material.lattice.c == 1.0 + assert material.lattice.alpha == 90.0 + assert material.lattice.beta == 90.0 + assert material.lattice.gamma == 90.0 + assert material.name == "New Material" + + # Test custom parameters + material = Material.create_empty( + a=2.0, + b=3.0, + c=4.0, + alpha=80.0, + beta=85.0, + gamma=95.0, + lattice_type="TRI", + name="Custom Empty" + ) + assert material.basis.elements.values == [] + assert material.basis.coordinates.values == [] + assert material.lattice.type == "TRI" + assert material.lattice.a == 2.0 + assert material.lattice.b == 3.0 + assert material.lattice.c == 4.0 + assert material.lattice.alpha == 80.0 + assert material.lattice.beta == 85.0 + assert material.lattice.gamma == 95.0 + assert material.name == "Custom Empty" + + # Test with only 'a' parameter + material = Material.create_empty(a=2.5) + assert material.lattice.a == 2.5 + assert material.lattice.b == 2.5 + assert material.lattice.c == 2.5 + From 00aee13db0b8c3bce83ed336f7e1dd4940631e02 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Jan 2025 17:11:52 -0800 Subject: [PATCH 03/20] update: test to sync cell with lattice --- tests/py/unit/test_material.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/py/unit/test_material.py b/tests/py/unit/test_material.py index a072494c..93655229 100644 --- a/tests/py/unit/test_material.py +++ b/tests/py/unit/test_material.py @@ -27,6 +27,30 @@ def test_basis_to_json(): # TODO: Add test to check if basis.cell is changed when lattice of material is changed, and vice versa +def test_basis_cell_lattice_sync(): + """Test synchronization between basis.cell and material.lattice""" + material = Material.create(Material.default_config) + + # Change lattice vectors + new_vectors = [ + [1.0, 0.0, 0.0], + [0.0, 2.0, 0.0], + [0.0, 0.0, 3.0] + ] + material.set_new_lattice_vectors(*new_vectors) + + # Verify basis.cell matches new lattice vectors + assertion_utils.assert_deep_almost_equal( + new_vectors, + material.basis.cell.vectors_as_array + ) + + assertion_utils.assert_deep_almost_equal( + new_vectors, + material.lattice.vectors + ) + + def test_create_empty(): """Test creating empty materials with different lattice parameters""" @@ -71,3 +95,4 @@ def test_create_empty(): assert material.lattice.b == 2.5 assert material.lattice.c == 2.5 + From 98c240c230337acd0a5756025471e231fee5fca6 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Jan 2025 17:11:52 -0800 Subject: [PATCH 04/20] update: test to sync cell with lattice --- tests/py/unit/test_material.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/py/unit/test_material.py b/tests/py/unit/test_material.py index 93655229..39ea058d 100644 --- a/tests/py/unit/test_material.py +++ b/tests/py/unit/test_material.py @@ -25,7 +25,29 @@ def test_basis_to_json(): assertion_utils.assert_deep_almost_equal(expected_basis_config, basis.to_json()) -# TODO: Add test to check if basis.cell is changed when lattice of material is changed, and vice versa +def test_basis_cell_lattice_sync(): + """Test synchronization between basis.cell and material.lattice""" + material = Material.create(Material.default_config) + + # Change lattice vectors + new_vectors = [ + [1.0, 0.0, 0.0], + [0.0, 2.0, 0.0], + [0.0, 0.0, 3.0] + ] + material.set_new_lattice_vectors(*new_vectors) + + # Verify basis.cell matches new lattice vectors + assertion_utils.assert_deep_almost_equal( + new_vectors, + material.basis.cell.vectors_as_array + ) + + assertion_utils.assert_deep_almost_equal( + new_vectors, + material.lattice.vectors + ) + def test_basis_cell_lattice_sync(): """Test synchronization between basis.cell and material.lattice""" From ac95cf976bebe29b7d3ed4997aa6d93fd7a26fc9 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Jan 2025 18:17:26 -0800 Subject: [PATCH 05/20] update: allow for lattice.vectors.a --- src/py/mat3ra/made/lattice.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/py/mat3ra/made/lattice.py b/src/py/mat3ra/made/lattice.py index e6fe59a4..34dce83b 100644 --- a/src/py/mat3ra/made/lattice.py +++ b/src/py/mat3ra/made/lattice.py @@ -11,6 +11,14 @@ DEFAULT_UNITS = {"length": "angstrom", "angle": "degree"} DEFAULT_TYPE = "TRI" +class LatticeVectors(BaseModel): + """ + A class to represent the lattice vectors. + """ + a: List[float] = [[1.0, 0.0, 0.0]] + b: List[float] = [[0.0, 1.0, 0.0]] + c: List[float] = [[0.0, 0.0, 1.0]] + class Lattice(RoundNumericValuesMixin, BaseModel): a: float = 1.0 @@ -23,7 +31,7 @@ class Lattice(RoundNumericValuesMixin, BaseModel): type: str = DEFAULT_TYPE @property - def vectors(self) -> List[List[float]]: + def vectors(self) -> LatticeVectors: a = self.a b = self.b c = self.c @@ -44,12 +52,12 @@ def vectors(self) -> List[List[float]]: cos_gamma_star = math.cos(gamma_star) sin_gamma_star = math.sin(gamma_star) - # Return the lattice matrix using the derived trigonometric values - return [ - [a * sin_beta, 0.0, a * cos_beta], - [-b * sin_alpha * cos_gamma_star, b * sin_alpha * sin_gamma_star, b * cos_alpha], - [0.0, 0.0, c], - ] + # Calculate the vectors + vector_a = [a * sin_beta, 0.0, a * cos_beta] + vector_b = [-b * sin_alpha * cos_gamma_star, b * sin_alpha * sin_gamma_star, b * cos_alpha] + vector_c = [0.0, 0.0, c] + + return LatticeVectors(a=vector_a, b=vector_b, c=vector_c) @classmethod def from_vectors_array( @@ -96,7 +104,9 @@ def clone(self, extra_context: Optional[Dict[str, Any]] = None) -> "Lattice": @property def vector_arrays(self) -> List[List[float]]: - return self.vectors + """Returns lattice vectors as a nested array""" + v = self.vectors + return [v.a, v.b, v.c] @property def cell(self) -> Cell: From 438d5729a0192cc76a4fef3daedcd3428577db46 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Jan 2025 18:17:42 -0800 Subject: [PATCH 06/20] update: use lattice.vecotrs.a --- src/py/mat3ra/made/tools/build/nanoribbon/builders.py | 10 +++++----- src/py/mat3ra/made/tools/modify.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/nanoribbon/builders.py b/src/py/mat3ra/made/tools/build/nanoribbon/builders.py index 5bdb9b0c..5027db56 100644 --- a/src/py/mat3ra/made/tools/build/nanoribbon/builders.py +++ b/src/py/mat3ra/made/tools/build/nanoribbon/builders.py @@ -73,11 +73,11 @@ def _calculate_cartesian_dimensions(config: NanoribbonConfiguration, material: M nanoribbon_length, nanoribbon_width = nanoribbon_width, nanoribbon_length vacuum_width, vacuum_length = vacuum_length, vacuum_width - length_cartesian = nanoribbon_length * np.dot(np.array(material.lattice.vectors[0]), np.array([1, 0, 0])) - width_cartesian = nanoribbon_width * np.dot(np.array(material.lattice.vectors[1]), np.array([0, 1, 0])) - height_cartesian = np.dot(np.array(material.lattice.vectors[2]), np.array([0, 0, 1])) - vacuum_length_cartesian = vacuum_length * np.dot(np.array(material.lattice.vectors[0]), np.array([1, 0, 0])) - vacuum_width_cartesian = vacuum_width * np.dot(np.array(material.lattice.vectors[1]), np.array([0, 1, 0])) + length_cartesian = nanoribbon_length * np.dot(np.array(material.lattice.vectors.a), np.array([1, 0, 0])) + width_cartesian = nanoribbon_width * np.dot(np.array(material.lattice.vectors.b), np.array([0, 1, 0])) + height_cartesian = np.dot(np.array(material.lattice.vectors.c), np.array([0, 0, 1])) + vacuum_length_cartesian = vacuum_length * np.dot(np.array(material.lattice.vectors.a), np.array([1, 0, 0])) + vacuum_width_cartesian = vacuum_width * np.dot(np.array(material.lattice.vectors.b), np.array([0, 1, 0])) return length_cartesian, width_cartesian, height_cartesian, vacuum_length_cartesian, vacuum_width_cartesian diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index e128aab1..ecd69238 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -201,7 +201,7 @@ def filter_by_layers( if central_atom_id is not None: center_coordinate = material.basis.coordinates.get_element_value_by_index(central_atom_id) vectors = material.lattice.vectors - direction_vector = vectors[2] + direction_vector = vectors.c def condition(coordinate): return is_coordinate_within_layer(coordinate, center_coordinate, direction_vector, layer_thickness) @@ -535,7 +535,7 @@ def remove_vacuum(material: Material, from_top=True, from_bottom=True, fixed_pad new_basis.to_cartesian() new_lattice = translated_material.lattice new_lattice.c = get_atomic_coordinates_extremum(translated_material, use_cartesian_coordinates=True) + fixed_padding - new_basis.cell.vector3 = new_lattice.vectors[2] + new_basis.cell.vector3 = new_lattice.vectors.c new_basis.to_crystal() new_material = material.clone() From e2dbbe596e0b1eea55ff2058c833ee8f47653cd0 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Jan 2025 18:17:49 -0800 Subject: [PATCH 07/20] update: fix tests --- tests/py/unit/test_material.py | 18 +++++++++++++++++- tests/py/unit/test_tools_build_interface.py | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/py/unit/test_material.py b/tests/py/unit/test_material.py index 39ea058d..96795dfe 100644 --- a/tests/py/unit/test_material.py +++ b/tests/py/unit/test_material.py @@ -45,7 +45,7 @@ def test_basis_cell_lattice_sync(): assertion_utils.assert_deep_almost_equal( new_vectors, - material.lattice.vectors + material.lattice.vector_arrays ) @@ -118,3 +118,19 @@ def test_create_empty(): assert material.lattice.c == 2.5 +def test_lattice_vectors_access(): + lattice = Lattice(a=2.0, b=3.0, c=4.0) + + # Test individual vector access + assert isinstance(lattice.vectors.a, list) + assert isinstance(lattice.vectors.b, list) + assert isinstance(lattice.vectors.c, list) + + # Test vector arrays access + arrays = lattice.vector_arrays + assert len(arrays) == 3 + assert arrays[0] == lattice.vectors.a + assert arrays[1] == lattice.vectors.b + assert arrays[2] == lattice.vectors.c + + diff --git a/tests/py/unit/test_tools_build_interface.py b/tests/py/unit/test_tools_build_interface.py index 7305a04f..35f7a373 100644 --- a/tests/py/unit/test_tools_build_interface.py +++ b/tests/py/unit/test_tools_build_interface.py @@ -64,7 +64,7 @@ def test_create_twisted_nanoribbon_interface(): expected_cell_vectors = [[15.102811, 0.0, 0.0], [0.0, 16.108175208, 0.0], [0.0, 0.0, 20.0]] expected_coordinate = [0.704207885, 0.522108183, 0.65] - assertion_utils.assert_deep_almost_equal(expected_cell_vectors, interface.lattice.vectors) + assertion_utils.assert_deep_almost_equal(expected_cell_vectors, interface.lattice.vector_arrays) assertion_utils.assert_deep_almost_equal(expected_coordinate, interface.basis.coordinates.values[42]) From ba80d7fd6e4d6a4818a2cb4fa13b143ae324adf5 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Jan 2025 18:22:28 -0800 Subject: [PATCH 08/20] update: fix perturbation --- src/py/mat3ra/made/tools/build/perturbation/builders.py | 2 +- tests/py/unit/test_tools_build_perturbation.py | 2 +- 2 files changed, 2 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 31d2f407..36caa00f 100644 --- a/src/py/mat3ra/made/tools/build/perturbation/builders.py +++ b/src/py/mat3ra/made/tools/build/perturbation/builders.py @@ -65,7 +65,7 @@ def create_perturbed_slab(self, configuration: PerturbationConfiguration) -> Mat class CellMatchingDistancePreservingSlabPerturbationBuilder(DistancePreservingSlabPerturbationBuilder): def _transform_lattice_vectors(self, configuration: PerturbationConfiguration) -> List[List[float]]: - cell_vectors = configuration.material.lattice.vectors + cell_vectors = configuration.material.lattice.vector_arrays return [configuration.perturbation_function_holder.transform_coordinates(coord) for coord in cell_vectors] def create_perturbed_slab(self, configuration: PerturbationConfiguration) -> Material: diff --git a/tests/py/unit/test_tools_build_perturbation.py b/tests/py/unit/test_tools_build_perturbation.py index c358970d..ec41ebae 100644 --- a/tests/py/unit/test_tools_build_perturbation.py +++ b/tests/py/unit/test_tools_build_perturbation.py @@ -46,4 +46,4 @@ def test_distance_preserved_sine_perturbation(): [-12.043583, 21.367367, 0.0], [0.0, 0.0, 20.0], ] - assertion_utils.assert_deep_almost_equal(expected_cell, perturbed_slab.lattice.vectors) + assertion_utils.assert_deep_almost_equal(expected_cell, perturbed_slab.lattice.vector_arrays) From 0c02d04cb1ca5fb66bb5c71454deafd2bcc89dcb Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Jan 2025 19:07:10 -0800 Subject: [PATCH 09/20] update: remove unused --- src/py/mat3ra/made/tools/utils/__init__.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/py/mat3ra/made/tools/utils/__init__.py b/src/py/mat3ra/made/tools/utils/__init__.py index 7240dab7..63ae9174 100644 --- a/src/py/mat3ra/made/tools/utils/__init__.py +++ b/src/py/mat3ra/made/tools/utils/__init__.py @@ -9,25 +9,6 @@ DEFAULT_SCALING_FACTOR = np.array([3, 3, 3]) DEFAULT_TRANSLATION_VECTOR = 1 / DEFAULT_SCALING_FACTOR - -# TODO: convert to accept ASE Atoms object -def translate_to_bottom_pymatgen_structure(structure: PymatgenStructure): - """ - Translate the structure to the bottom of the cell. - Args: - structure (PymatgenStructure): The pymatgen Structure object to translate. - - Returns: - PymatgenStructure: The translated pymatgen Structure object. - """ - min_c = min(site.c for site in structure) - translation_vector = [0, 0, -min_c] - translated_structure = structure.copy() - for site in translated_structure: - site.coords += translation_vector - return translated_structure - - def decorator_convert_2x2_to_3x3(func: Callable) -> Callable: """ Decorator to convert a 2x2 matrix to a 3x3 matrix. From 32be2c1a93386317e34d168b253f4ffe67b2be62 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Jan 2025 19:14:03 -0800 Subject: [PATCH 10/20] update: add tolerance --- src/py/mat3ra/made/tools/modify.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index ecd69238..d4bab7a2 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -1,6 +1,7 @@ from typing import Callable, List, Literal, Optional, Tuple, Union import numpy as np +from fontTools.misc.bezierTools import epsilon from mat3ra.made.material import Material from .analyze.other import ( @@ -40,7 +41,7 @@ def filter_by_label(material: Material, label: Union[int, str]) -> Material: def translate_to_z_level( - material: Material, z_level: Optional[Literal["top", "bottom", "center"]] = "bottom" + material: Material, z_level: Optional[Literal["top", "bottom", "center"]] = "bottom", tolerance: float = 1e-6 ) -> Material: """ Translate atoms to the specified z-level. @@ -48,15 +49,16 @@ def translate_to_z_level( Args: material (Material): The material object to normalize. z_level (str): The z-level to translate the atoms to (top, bottom, center) + tolerance (float): The tolerance value to avoid moving past unit cell. Returns: Material: The translated material object. """ min_z = get_atomic_coordinates_extremum(material, "min") max_z = get_atomic_coordinates_extremum(material) if z_level == "top": - material = translate_by_vector(material, vector=[0, 0, 1 - max_z]) + material = translate_by_vector(material, vector=[0, 0, 1 - max_z - tolerance]) elif z_level == "bottom": - material = translate_by_vector(material, vector=[0, 0, -min_z]) + material = translate_by_vector(material, vector=[0, 0, - min_z + tolerance]) elif z_level == "center": material = translate_by_vector(material, vector=[0, 0, (1 - min_z - max_z) / 2]) return material From d975ab7cb6e2b92545fe6cbb703ba20dd027c807 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Jan 2025 19:16:06 -0800 Subject: [PATCH 11/20] update: adjust test --- tests/py/unit/test_tools_modify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/py/unit/test_tools_modify.py b/tests/py/unit/test_tools_modify.py index 18926dea..07b8fe3f 100644 --- a/tests/py/unit/test_tools_modify.py +++ b/tests/py/unit/test_tools_modify.py @@ -158,7 +158,7 @@ def test_remove_vacuum(): # to compare correctly, we need to translate the expected material to the bottom # as it down when setting vacuum to 0 material = Material(SI_SLAB) - material_down = translate_to_z_level(material, z_level="bottom") + material_down = translate_to_z_level(material, z_level="bottom", tolerance=0) assertion_utils.assert_deep_almost_equal(material_down.to_json(), material_with_set_vacuum.to_json()) From d90a534cb1f43f72f95431d5de203adb4674cc71 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Jan 2025 19:26:46 -0800 Subject: [PATCH 12/20] chore: lint fix --- src/py/mat3ra/made/lattice.py | 8 ++-- src/py/mat3ra/made/material.py | 13 ++--- src/py/mat3ra/made/tools/modify.py | 3 +- src/py/mat3ra/made/tools/utils/__init__.py | 1 + tests/py/unit/test_material.py | 56 +++------------------- 5 files changed, 17 insertions(+), 64 deletions(-) diff --git a/src/py/mat3ra/made/lattice.py b/src/py/mat3ra/made/lattice.py index 34dce83b..9ea5d600 100644 --- a/src/py/mat3ra/made/lattice.py +++ b/src/py/mat3ra/made/lattice.py @@ -11,13 +11,15 @@ DEFAULT_UNITS = {"length": "angstrom", "angle": "degree"} DEFAULT_TYPE = "TRI" + class LatticeVectors(BaseModel): """ A class to represent the lattice vectors. """ - a: List[float] = [[1.0, 0.0, 0.0]] - b: List[float] = [[0.0, 1.0, 0.0]] - c: List[float] = [[0.0, 0.0, 1.0]] + + a: List[float] = [1.0, 0.0, 0.0] + b: List[float] = [0.0, 1.0, 0.0] + c: List[float] = [0.0, 0.0, 1.0] class Lattice(RoundNumericValuesMixin, BaseModel): diff --git a/src/py/mat3ra/made/material.py b/src/py/mat3ra/made/material.py index a985d472..957f360d 100644 --- a/src/py/mat3ra/made/material.py +++ b/src/py/mat3ra/made/material.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Union, Optional +from typing import Any, Dict, List, Optional, Union from mat3ra.code.constants import AtomicCoordinateUnits, Units from mat3ra.code.entity import HasDescriptionHasMetadataNamedDefaultableInMemoryEntity @@ -113,7 +113,6 @@ def add_atom(self, element: str, coordinate: List[float], use_cartesian_coordina new_basis.add_atom(element, coordinate, use_cartesian_coordinates) self.basis = new_basis - @classmethod def create_empty( cls, @@ -124,7 +123,7 @@ def create_empty( beta: float = 90.0, gamma: float = 90.0, lattice_type: str = "CUB", - name: str = "New Material" + name: str = "New Material", ) -> "Material": """ Create an empty material with specified lattice parameters. @@ -162,13 +161,9 @@ def create_empty( "units": { "length": Units.angstrom, "angle": Units.degree, - } + }, } - config = { - "name": name, - "basis": basis_config, - "lattice": lattice_config - } + config = {"name": name, "basis": basis_config, "lattice": lattice_config} return cls(config) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index d4bab7a2..24d04b03 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -1,7 +1,6 @@ from typing import Callable, List, Literal, Optional, Tuple, Union import numpy as np -from fontTools.misc.bezierTools import epsilon from mat3ra.made.material import Material from .analyze.other import ( @@ -58,7 +57,7 @@ def translate_to_z_level( if z_level == "top": material = translate_by_vector(material, vector=[0, 0, 1 - max_z - tolerance]) elif z_level == "bottom": - material = translate_by_vector(material, vector=[0, 0, - min_z + tolerance]) + material = translate_by_vector(material, vector=[0, 0, -min_z + tolerance]) elif z_level == "center": material = translate_by_vector(material, vector=[0, 0, (1 - min_z - max_z) / 2]) return material diff --git a/src/py/mat3ra/made/tools/utils/__init__.py b/src/py/mat3ra/made/tools/utils/__init__.py index 63ae9174..23ed7d99 100644 --- a/src/py/mat3ra/made/tools/utils/__init__.py +++ b/src/py/mat3ra/made/tools/utils/__init__.py @@ -9,6 +9,7 @@ DEFAULT_SCALING_FACTOR = np.array([3, 3, 3]) DEFAULT_TRANSLATION_VECTOR = 1 / DEFAULT_SCALING_FACTOR + def decorator_convert_2x2_to_3x3(func: Callable) -> Callable: """ Decorator to convert a 2x2 matrix to a 3x3 matrix. diff --git a/tests/py/unit/test_material.py b/tests/py/unit/test_material.py index 96795dfe..61811991 100644 --- a/tests/py/unit/test_material.py +++ b/tests/py/unit/test_material.py @@ -30,48 +30,13 @@ def test_basis_cell_lattice_sync(): material = Material.create(Material.default_config) # Change lattice vectors - new_vectors = [ - [1.0, 0.0, 0.0], - [0.0, 2.0, 0.0], - [0.0, 0.0, 3.0] - ] + new_vectors = [[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 3.0]] material.set_new_lattice_vectors(*new_vectors) # Verify basis.cell matches new lattice vectors - assertion_utils.assert_deep_almost_equal( - new_vectors, - material.basis.cell.vectors_as_array - ) - - assertion_utils.assert_deep_almost_equal( - new_vectors, - material.lattice.vector_arrays - ) - - -def test_basis_cell_lattice_sync(): - """Test synchronization between basis.cell and material.lattice""" - material = Material.create(Material.default_config) - - # Change lattice vectors - new_vectors = [ - [1.0, 0.0, 0.0], - [0.0, 2.0, 0.0], - [0.0, 0.0, 3.0] - ] - material.set_new_lattice_vectors(*new_vectors) - - # Verify basis.cell matches new lattice vectors - assertion_utils.assert_deep_almost_equal( - new_vectors, - material.basis.cell.vectors_as_array - ) - - assertion_utils.assert_deep_almost_equal( - new_vectors, - material.lattice.vectors - ) + assertion_utils.assert_deep_almost_equal(new_vectors, material.basis.cell.vectors_as_array) + assertion_utils.assert_deep_almost_equal(new_vectors, material.lattice.vector_arrays) def test_create_empty(): @@ -91,14 +56,7 @@ def test_create_empty(): # Test custom parameters material = Material.create_empty( - a=2.0, - b=3.0, - c=4.0, - alpha=80.0, - beta=85.0, - gamma=95.0, - lattice_type="TRI", - name="Custom Empty" + a=2.0, b=3.0, c=4.0, alpha=80.0, beta=85.0, gamma=95.0, lattice_type="TRI", name="Custom Empty" ) assert material.basis.elements.values == [] assert material.basis.coordinates.values == [] @@ -120,17 +78,15 @@ def test_create_empty(): def test_lattice_vectors_access(): lattice = Lattice(a=2.0, b=3.0, c=4.0) - + # Test individual vector access assert isinstance(lattice.vectors.a, list) assert isinstance(lattice.vectors.b, list) assert isinstance(lattice.vectors.c, list) - + # Test vector arrays access arrays = lattice.vector_arrays assert len(arrays) == 3 assert arrays[0] == lattice.vectors.a assert arrays[1] == lattice.vectors.b assert arrays[2] == lattice.vectors.c - - From baec628c1a9a0e48f7ef8afeb952fe7eeaa4956b Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Jan 2025 19:52:46 -0800 Subject: [PATCH 13/20] update: add tolerance to test --- tests/py/unit/test_tools_modify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/py/unit/test_tools_modify.py b/tests/py/unit/test_tools_modify.py index 07b8fe3f..7da205bc 100644 --- a/tests/py/unit/test_tools_modify.py +++ b/tests/py/unit/test_tools_modify.py @@ -158,9 +158,9 @@ def test_remove_vacuum(): # to compare correctly, we need to translate the expected material to the bottom # as it down when setting vacuum to 0 material = Material(SI_SLAB) - material_down = translate_to_z_level(material, z_level="bottom", tolerance=0) + material_down = translate_to_z_level(material, z_level="bottom") - assertion_utils.assert_deep_almost_equal(material_down.to_json(), material_with_set_vacuum.to_json()) + assertion_utils.assert_deep_almost_equal(material_down.to_json(), material_with_set_vacuum.to_json(), atol=1e-3) def test_rotate(): From bd174a5f0391988b4e129c484784b409aa0d7ebd Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 13 Jan 2025 14:11:28 -0800 Subject: [PATCH 14/20] update: add coord numbers distribution func --- .../made/tools/build/passivation/__init__.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/py/mat3ra/made/tools/build/passivation/__init__.py b/src/py/mat3ra/made/tools/build/passivation/__init__.py index 9d467b1e..638ea92f 100644 --- a/src/py/mat3ra/made/tools/build/passivation/__init__.py +++ b/src/py/mat3ra/made/tools/build/passivation/__init__.py @@ -1,4 +1,5 @@ -from typing import Union, List +from collections import Counter +from typing import Union, List, Dict from mat3ra.made.material import Material from .configuration import PassivationConfiguration @@ -36,3 +37,23 @@ def get_unique_coordination_numbers( material_with_crystal_sites = MaterialWithCrystalSites.from_material(configuration.slab) material_with_crystal_sites.analyze() return material_with_crystal_sites.get_unique_coordination_numbers(cutoff=cutoff) + +def get_coordination_numbers_distribution( + configuration: PassivationConfiguration, + cutoff: float = 3.0, +) -> Dict[int, int]: + """ + Get the unique coordination numbers for the provided passivation configuration and cutoff radius. + + Args: + configuration (PassivationConfiguration): The configuration object. + cutoff (float): The cutoff radius for defining neighbors. + Returns: + set: The unique coordination numbers. + """ + material_with_crystal_sites = MaterialWithCrystalSites.from_material(configuration.slab) + material_with_crystal_sites.analyze() + coordinatation_numbers = material_with_crystal_sites.get_coordination_numbers(cutoff=cutoff) + sorted_coordinatation_numbers = sorted(coordinatation_numbers.values) + distribution = dict(Counter(sorted_coordinatation_numbers)) + return distribution From c8f7a07e199e398d1e327bc60c5b4663b71df6b2 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 13 Jan 2025 17:28:35 -0800 Subject: [PATCH 15/20] update: add tolerances to filter funcs --- src/py/mat3ra/made/tools/modify.py | 45 ++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index 24d04b03..3078846d 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -241,6 +241,7 @@ def filter_by_sphere( center_coordinate: List[float] = [0, 0, 0], central_atom_id: Optional[int] = None, radius: float = 1, + tolerance: float = 0.0, invert: bool = False, ) -> Material: """ @@ -250,6 +251,7 @@ def filter_by_sphere( material (Material): The material object to filter. central_atom_id (int): Index of the central atom. radius (float): Radius of the sphere in angstroms. + tolerance (float): The tolerance value to include atoms on the edge of the sphere. invert (bool): Whether to invert the selection. Returns: @@ -259,7 +261,7 @@ def filter_by_sphere( material=material, atom_index=central_atom_id, coordinate=center_coordinate, - radius=radius, + radius=radius + tolerance, ) return filter_by_ids(material, ids, invert=invert) @@ -269,6 +271,7 @@ def filter_by_circle_projection( x: float = 0.5, y: float = 0.5, r: float = 0.25, + tolerance: float = 0.0, use_cartesian_coordinates: bool = False, invert_selection: bool = False, ) -> Material: @@ -280,6 +283,7 @@ def filter_by_circle_projection( x (float): The x-coordinate of the circle center. y (float): The y-coordinate of the circle center. r (float): The radius of the circle. + tolerance (float): The tolerance value to include atoms on the edge of the circle. use_cartesian_coordinates (bool): Whether to use cartesian coordinates invert_selection (bool): Whether to invert the selection. @@ -288,7 +292,7 @@ def filter_by_circle_projection( """ def condition(coordinate): - return is_coordinate_in_cylinder(coordinate, [x, y, 0], r, min_z=0, max_z=1) + return is_coordinate_in_cylinder(coordinate, [x, y, 0], r + tolerance, min_z=0, max_z=1) return filter_by_condition_on_coordinates( material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection @@ -301,6 +305,7 @@ def filter_by_cylinder( min_z: Optional[float] = None, max_z: Optional[float] = None, radius: float = 0.25, + tolerance: float = 0.0, use_cartesian_coordinates: bool = False, invert_selection: bool = False, ) -> Material: @@ -332,7 +337,7 @@ def filter_by_cylinder( ) def condition(coordinate): - return is_coordinate_in_cylinder(coordinate, center_position, radius, min_z, max_z) + return is_coordinate_in_cylinder(coordinate, center_position, radius + tolerance, min_z - tolerance, max_z + tolerance) return filter_by_condition_on_coordinates( material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection @@ -343,6 +348,7 @@ def filter_by_rectangle_projection( material: Material, min_coordinate: List[float] = [0, 0], max_coordinate: List[float] = [1, 1], + tolerance: float = 0.0, use_cartesian_coordinates: bool = False, invert_selection: bool = False, ) -> Material: @@ -353,6 +359,7 @@ def filter_by_rectangle_projection( material (Material): The material object to filter. min_coordinate (List[float]): The minimum coordinate of the rectangle. max_coordinate (List[float]): The maximum coordinate of the rectangle. + tolerance (float): The tolerance value to include atoms on the edges of the rectangle. use_cartesian_coordinates (bool): Whether to use cartesian coordinates invert_selection (bool): Whether to invert the selection. @@ -361,8 +368,8 @@ def filter_by_rectangle_projection( """ min_z = get_atomic_coordinates_extremum(material, "min", "z", use_cartesian_coordinates=use_cartesian_coordinates) max_z = get_atomic_coordinates_extremum(material, "max", "z", use_cartesian_coordinates=use_cartesian_coordinates) - min_coordinate = min_coordinate[:2] + [min_z] - max_coordinate = max_coordinate[:2] + [max_z] + min_coordinate = [c - tolerance for c in min_coordinate[:2]] + [min_z] + max_coordinate = [c + tolerance for c in max_coordinate[:2]] + [max_z] def condition(coordinate): return is_coordinate_in_box(coordinate, min_coordinate, max_coordinate) @@ -376,6 +383,7 @@ def filter_by_box( material: Material, min_coordinate: Optional[List[float]] = None, max_coordinate: Optional[List[float]] = None, + tolerance: float = 0.0, use_cartesian_coordinates: bool = False, invert_selection: bool = False, ) -> Material: @@ -386,6 +394,7 @@ def filter_by_box( material (Material): The material to filter. min_coordinate (List[float], optional): The minimum coordinate of the box. Defaults to material's min. max_coordinate (List[float], optional): The maximum coordinate of the box. Defaults to material's max. + tolerance (float): The tolerance value to include atoms on the edges of the box. use_cartesian_coordinates (bool): Whether to use cartesian coordinates. invert_selection (bool): Whether to invert the selection. @@ -397,6 +406,9 @@ def filter_by_box( min_coordinate = min_coordinate if min_coordinate is not None else default_min max_coordinate = max_coordinate if max_coordinate is not None else default_max + min_coordinate = [c - tolerance for c in min_coordinate] + max_coordinate = [c + tolerance for c in max_coordinate] + def condition(coordinate): return is_coordinate_in_box(coordinate, min_coordinate, max_coordinate) @@ -412,6 +424,7 @@ def filter_by_triangle_projection( coordinate_3: Optional[List[float]] = None, min_z: Optional[float] = None, max_z: Optional[float] = None, + tolerance: float = 0.0, use_cartesian_coordinates: bool = False, invert_selection: bool = False, ) -> Material: @@ -425,6 +438,7 @@ def filter_by_triangle_projection( coordinate_3 (List[float], optional): Third vertex of the triangle. Defaults to material's corner. min_z (float, optional): Lower z-limit. Defaults to material's min z. max_z (float, optional): Upper z-limit. Defaults to material's max z. + tolerance (float): The tolerance value to include atoms on the top and bottom faces of the prism. use_cartesian_coordinates (bool): Whether to use cartesian coordinates. invert_selection (bool): Whether to invert the selection. @@ -437,17 +451,20 @@ def filter_by_triangle_projection( coordinate_2 = coordinate_2 if coordinate_2 is not None else [default_min[0], default_max[1]] coordinate_3 = coordinate_3 if coordinate_3 is not None else [default_max[0], default_min[1]] - if min_z is None: - min_z = get_atomic_coordinates_extremum( - material, "min", "z", use_cartesian_coordinates=use_cartesian_coordinates - ) - if max_z is None: - max_z = get_atomic_coordinates_extremum( - material, "max", "z", use_cartesian_coordinates=use_cartesian_coordinates - ) + if min_z is not None: + min_z -= tolerance + if max_z is not None: + max_z += tolerance def condition(coordinate): - return is_coordinate_in_triangular_prism(coordinate, coordinate_1, coordinate_2, coordinate_3, min_z, max_z) + return is_coordinate_in_triangular_prism( + coordinate, + coordinate_1, + coordinate_2, + coordinate_3, + min_z, + max_z, + ) return filter_by_condition_on_coordinates( material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection From 35ca52a2bd03401407ffee846bd4ac7c536bdeed Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 13 Jan 2025 17:50:26 -0800 Subject: [PATCH 16/20] update: fix erroneous error --- src/py/mat3ra/made/tools/modify.py | 148 +++++++++++++++-------------- 1 file changed, 79 insertions(+), 69 deletions(-) diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index 3078846d..d452ac5e 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -40,7 +40,7 @@ def filter_by_label(material: Material, label: Union[int, str]) -> Material: def translate_to_z_level( - material: Material, z_level: Optional[Literal["top", "bottom", "center"]] = "bottom", tolerance: float = 1e-6 + material: Material, z_level: Optional[Literal["top", "bottom", "center"]] = "bottom", tolerance: float = 1e-6 ) -> Material: """ Translate atoms to the specified z-level. @@ -64,9 +64,9 @@ def translate_to_z_level( def translate_by_vector( - material: Material, - vector: Optional[List[float]] = None, - use_cartesian_coordinates: bool = False, + material: Material, + vector: Optional[List[float]] = None, + use_cartesian_coordinates: bool = False, ) -> Material: """ Translate atoms by a vector. @@ -151,10 +151,10 @@ def filter_by_ids(material: Material, ids: List[int], invert: bool = False) -> M def filter_by_condition_on_coordinates( - material: Material, - condition: Callable[[List[float]], bool], - use_cartesian_coordinates: bool = False, - invert_selection: bool = False, + material: Material, + condition: Callable[[List[float]], bool], + use_cartesian_coordinates: bool = False, + invert_selection: bool = False, ) -> Material: """ Filter atoms based on a condition on their coordinates. @@ -180,11 +180,11 @@ def filter_by_condition_on_coordinates( def filter_by_layers( - material: Material, - center_coordinate: List[float] = [0, 0, 0], - central_atom_id: Optional[int] = None, - layer_thickness: float = 1.0, - invert_selection: bool = False, + material: Material, + center_coordinate: List[float] = [0, 0, 0], + central_atom_id: Optional[int] = None, + layer_thickness: float = 1.0, + invert_selection: bool = False, ) -> Material: """ Filter out atoms within a specified layer thickness of a central atom along c-vector direction. @@ -237,12 +237,12 @@ def get_default_min_max(material: Material, use_cartesian_coordinates: bool) -> def filter_by_sphere( - material: Material, - center_coordinate: List[float] = [0, 0, 0], - central_atom_id: Optional[int] = None, - radius: float = 1, - tolerance: float = 0.0, - invert: bool = False, + material: Material, + center_coordinate: List[float] = [0, 0, 0], + central_atom_id: Optional[int] = None, + radius: float = 1, + tolerance: float = 0.0, + invert: bool = False, ) -> Material: """ Filter out atoms within a specified radius of a central atom considering periodic boundary conditions. @@ -267,13 +267,13 @@ def filter_by_sphere( def filter_by_circle_projection( - material: Material, - x: float = 0.5, - y: float = 0.5, - r: float = 0.25, - tolerance: float = 0.0, - use_cartesian_coordinates: bool = False, - invert_selection: bool = False, + material: Material, + x: float = 0.5, + y: float = 0.5, + r: float = 0.25, + tolerance: float = 0.0, + use_cartesian_coordinates: bool = False, + invert_selection: bool = False, ) -> Material: """ Get material with atoms that are within or outside an XY circle projection. @@ -300,14 +300,14 @@ def condition(coordinate): def filter_by_cylinder( - material: Material, - center_position: Optional[List[float]] = None, - min_z: Optional[float] = None, - max_z: Optional[float] = None, - radius: float = 0.25, - tolerance: float = 0.0, - use_cartesian_coordinates: bool = False, - invert_selection: bool = False, + material: Material, + center_position: Optional[List[float]] = None, + min_z: Optional[float] = None, + max_z: Optional[float] = None, + radius: float = 0.25, + tolerance: float = 0.0, + use_cartesian_coordinates: bool = False, + invert_selection: bool = False, ) -> Material: """ Get material with atoms that are within or outside a cylinder. @@ -337,7 +337,8 @@ def filter_by_cylinder( ) def condition(coordinate): - return is_coordinate_in_cylinder(coordinate, center_position, radius + tolerance, min_z - tolerance, max_z + tolerance) + return is_coordinate_in_cylinder(coordinate, center_position, radius + tolerance, min_z - tolerance, + max_z + tolerance) return filter_by_condition_on_coordinates( material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection @@ -345,12 +346,12 @@ def condition(coordinate): def filter_by_rectangle_projection( - material: Material, - min_coordinate: List[float] = [0, 0], - max_coordinate: List[float] = [1, 1], - tolerance: float = 0.0, - use_cartesian_coordinates: bool = False, - invert_selection: bool = False, + material: Material, + min_coordinate: List[float] = [0, 0], + max_coordinate: List[float] = [1, 1], + tolerance: float = 0.0, + use_cartesian_coordinates: bool = False, + invert_selection: bool = False, ) -> Material: """ Get material with atoms that are within or outside an XY rectangle projection. @@ -380,12 +381,12 @@ def condition(coordinate): def filter_by_box( - material: Material, - min_coordinate: Optional[List[float]] = None, - max_coordinate: Optional[List[float]] = None, - tolerance: float = 0.0, - use_cartesian_coordinates: bool = False, - invert_selection: bool = False, + material: Material, + min_coordinate: Optional[List[float]] = None, + max_coordinate: Optional[List[float]] = None, + tolerance: float = 0.0, + use_cartesian_coordinates: bool = False, + invert_selection: bool = False, ) -> Material: """ Get material with atoms that are within or outside an XYZ box. @@ -418,15 +419,15 @@ def condition(coordinate): def filter_by_triangle_projection( - material: Material, - coordinate_1: Optional[List[float]] = None, - coordinate_2: Optional[List[float]] = None, - coordinate_3: Optional[List[float]] = None, - min_z: Optional[float] = None, - max_z: Optional[float] = None, - tolerance: float = 0.0, - use_cartesian_coordinates: bool = False, - invert_selection: bool = False, + material: Material, + coordinate_1: Optional[List[float]] = None, + coordinate_2: Optional[List[float]] = None, + coordinate_3: Optional[List[float]] = None, + min_z: Optional[float] = None, + max_z: Optional[float] = None, + tolerance: float = 0.0, + use_cartesian_coordinates: bool = False, + invert_selection: bool = False, ) -> Material: """ Get material with atoms that are within or outside a triangular prism. @@ -451,18 +452,27 @@ def filter_by_triangle_projection( coordinate_2 = coordinate_2 if coordinate_2 is not None else [default_min[0], default_max[1]] coordinate_3 = coordinate_3 if coordinate_3 is not None else [default_max[0], default_min[1]] - if min_z is not None: + if min_z is None: + min_z = get_atomic_coordinates_extremum( + material, "min", "z", use_cartesian_coordinates=use_cartesian_coordinates + ) + else: min_z -= tolerance - if max_z is not None: + + if max_z is None: + max_z = get_atomic_coordinates_extremum( + material, "max", "z", use_cartesian_coordinates=use_cartesian_coordinates + ) + else: max_z += tolerance def condition(coordinate): return is_coordinate_in_triangular_prism( - coordinate, - coordinate_1, - coordinate_2, - coordinate_3, - min_z, + coordinate, + coordinate_1, + coordinate_2, + coordinate_3, + min_z, max_z, ) @@ -593,10 +603,10 @@ def rotate(material: Material, axis: List[int], angle: float, wrap: bool = True, def interface_displace_part( - interface: Material, - displacement: List[float], - label: InterfacePartsEnum = InterfacePartsEnum.FILM, - use_cartesian_coordinates=True, + interface: Material, + displacement: List[float], + label: InterfacePartsEnum = InterfacePartsEnum.FILM, + use_cartesian_coordinates=True, ) -> Material: """ Displace atoms in an interface along a certain direction. @@ -629,8 +639,8 @@ def interface_displace_part( def interface_get_part( - interface: Material, - part: InterfacePartsEnum = InterfacePartsEnum.FILM, + interface: Material, + part: InterfacePartsEnum = InterfacePartsEnum.FILM, ) -> Material: if interface.metadata["build"]["configuration"]["type"] != "InterfaceConfiguration": raise ValueError("The material is not an interface.") From c821d1d0323d9273d98f225f557a4f4bbbff4c50 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 14 Jan 2025 19:11:44 -0800 Subject: [PATCH 17/20] chore: lint fix --- .../made/tools/build/passivation/__init__.py | 1 + src/py/mat3ra/made/tools/modify.py | 127 +++++++++--------- 2 files changed, 65 insertions(+), 63 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/passivation/__init__.py b/src/py/mat3ra/made/tools/build/passivation/__init__.py index 638ea92f..871e3448 100644 --- a/src/py/mat3ra/made/tools/build/passivation/__init__.py +++ b/src/py/mat3ra/made/tools/build/passivation/__init__.py @@ -38,6 +38,7 @@ def get_unique_coordination_numbers( material_with_crystal_sites.analyze() return material_with_crystal_sites.get_unique_coordination_numbers(cutoff=cutoff) + def get_coordination_numbers_distribution( configuration: PassivationConfiguration, cutoff: float = 3.0, diff --git a/src/py/mat3ra/made/tools/modify.py b/src/py/mat3ra/made/tools/modify.py index d452ac5e..0ecd7546 100644 --- a/src/py/mat3ra/made/tools/modify.py +++ b/src/py/mat3ra/made/tools/modify.py @@ -40,7 +40,7 @@ def filter_by_label(material: Material, label: Union[int, str]) -> Material: def translate_to_z_level( - material: Material, z_level: Optional[Literal["top", "bottom", "center"]] = "bottom", tolerance: float = 1e-6 + material: Material, z_level: Optional[Literal["top", "bottom", "center"]] = "bottom", tolerance: float = 1e-6 ) -> Material: """ Translate atoms to the specified z-level. @@ -64,9 +64,9 @@ def translate_to_z_level( def translate_by_vector( - material: Material, - vector: Optional[List[float]] = None, - use_cartesian_coordinates: bool = False, + material: Material, + vector: Optional[List[float]] = None, + use_cartesian_coordinates: bool = False, ) -> Material: """ Translate atoms by a vector. @@ -151,10 +151,10 @@ def filter_by_ids(material: Material, ids: List[int], invert: bool = False) -> M def filter_by_condition_on_coordinates( - material: Material, - condition: Callable[[List[float]], bool], - use_cartesian_coordinates: bool = False, - invert_selection: bool = False, + material: Material, + condition: Callable[[List[float]], bool], + use_cartesian_coordinates: bool = False, + invert_selection: bool = False, ) -> Material: """ Filter atoms based on a condition on their coordinates. @@ -180,11 +180,11 @@ def filter_by_condition_on_coordinates( def filter_by_layers( - material: Material, - center_coordinate: List[float] = [0, 0, 0], - central_atom_id: Optional[int] = None, - layer_thickness: float = 1.0, - invert_selection: bool = False, + material: Material, + center_coordinate: List[float] = [0, 0, 0], + central_atom_id: Optional[int] = None, + layer_thickness: float = 1.0, + invert_selection: bool = False, ) -> Material: """ Filter out atoms within a specified layer thickness of a central atom along c-vector direction. @@ -237,12 +237,12 @@ def get_default_min_max(material: Material, use_cartesian_coordinates: bool) -> def filter_by_sphere( - material: Material, - center_coordinate: List[float] = [0, 0, 0], - central_atom_id: Optional[int] = None, - radius: float = 1, - tolerance: float = 0.0, - invert: bool = False, + material: Material, + center_coordinate: List[float] = [0, 0, 0], + central_atom_id: Optional[int] = None, + radius: float = 1, + tolerance: float = 0.0, + invert: bool = False, ) -> Material: """ Filter out atoms within a specified radius of a central atom considering periodic boundary conditions. @@ -267,13 +267,13 @@ def filter_by_sphere( def filter_by_circle_projection( - material: Material, - x: float = 0.5, - y: float = 0.5, - r: float = 0.25, - tolerance: float = 0.0, - use_cartesian_coordinates: bool = False, - invert_selection: bool = False, + material: Material, + x: float = 0.5, + y: float = 0.5, + r: float = 0.25, + tolerance: float = 0.0, + use_cartesian_coordinates: bool = False, + invert_selection: bool = False, ) -> Material: """ Get material with atoms that are within or outside an XY circle projection. @@ -300,14 +300,14 @@ def condition(coordinate): def filter_by_cylinder( - material: Material, - center_position: Optional[List[float]] = None, - min_z: Optional[float] = None, - max_z: Optional[float] = None, - radius: float = 0.25, - tolerance: float = 0.0, - use_cartesian_coordinates: bool = False, - invert_selection: bool = False, + material: Material, + center_position: Optional[List[float]] = None, + min_z: Optional[float] = None, + max_z: Optional[float] = None, + radius: float = 0.25, + tolerance: float = 0.0, + use_cartesian_coordinates: bool = False, + invert_selection: bool = False, ) -> Material: """ Get material with atoms that are within or outside a cylinder. @@ -337,8 +337,9 @@ def filter_by_cylinder( ) def condition(coordinate): - return is_coordinate_in_cylinder(coordinate, center_position, radius + tolerance, min_z - tolerance, - max_z + tolerance) + return is_coordinate_in_cylinder( + coordinate, center_position, radius + tolerance, min_z - tolerance, max_z + tolerance + ) return filter_by_condition_on_coordinates( material, condition, use_cartesian_coordinates=use_cartesian_coordinates, invert_selection=invert_selection @@ -346,12 +347,12 @@ def condition(coordinate): def filter_by_rectangle_projection( - material: Material, - min_coordinate: List[float] = [0, 0], - max_coordinate: List[float] = [1, 1], - tolerance: float = 0.0, - use_cartesian_coordinates: bool = False, - invert_selection: bool = False, + material: Material, + min_coordinate: List[float] = [0, 0], + max_coordinate: List[float] = [1, 1], + tolerance: float = 0.0, + use_cartesian_coordinates: bool = False, + invert_selection: bool = False, ) -> Material: """ Get material with atoms that are within or outside an XY rectangle projection. @@ -381,12 +382,12 @@ def condition(coordinate): def filter_by_box( - material: Material, - min_coordinate: Optional[List[float]] = None, - max_coordinate: Optional[List[float]] = None, - tolerance: float = 0.0, - use_cartesian_coordinates: bool = False, - invert_selection: bool = False, + material: Material, + min_coordinate: Optional[List[float]] = None, + max_coordinate: Optional[List[float]] = None, + tolerance: float = 0.0, + use_cartesian_coordinates: bool = False, + invert_selection: bool = False, ) -> Material: """ Get material with atoms that are within or outside an XYZ box. @@ -419,15 +420,15 @@ def condition(coordinate): def filter_by_triangle_projection( - material: Material, - coordinate_1: Optional[List[float]] = None, - coordinate_2: Optional[List[float]] = None, - coordinate_3: Optional[List[float]] = None, - min_z: Optional[float] = None, - max_z: Optional[float] = None, - tolerance: float = 0.0, - use_cartesian_coordinates: bool = False, - invert_selection: bool = False, + material: Material, + coordinate_1: Optional[List[float]] = None, + coordinate_2: Optional[List[float]] = None, + coordinate_3: Optional[List[float]] = None, + min_z: Optional[float] = None, + max_z: Optional[float] = None, + tolerance: float = 0.0, + use_cartesian_coordinates: bool = False, + invert_selection: bool = False, ) -> Material: """ Get material with atoms that are within or outside a triangular prism. @@ -603,10 +604,10 @@ def rotate(material: Material, axis: List[int], angle: float, wrap: bool = True, def interface_displace_part( - interface: Material, - displacement: List[float], - label: InterfacePartsEnum = InterfacePartsEnum.FILM, - use_cartesian_coordinates=True, + interface: Material, + displacement: List[float], + label: InterfacePartsEnum = InterfacePartsEnum.FILM, + use_cartesian_coordinates=True, ) -> Material: """ Displace atoms in an interface along a certain direction. @@ -639,8 +640,8 @@ def interface_displace_part( def interface_get_part( - interface: Material, - part: InterfacePartsEnum = InterfacePartsEnum.FILM, + interface: Material, + part: InterfacePartsEnum = InterfacePartsEnum.FILM, ) -> Material: if interface.metadata["build"]["configuration"]["type"] != "InterfaceConfiguration": raise ValueError("The material is not an interface.") From 34da867a5c93d684234ad9250049b1e10bc3b3a3 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 14 Jan 2025 19:36:44 -0800 Subject: [PATCH 18/20] update: tests --- tests/py/unit/test_material.py | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/tests/py/unit/test_material.py b/tests/py/unit/test_material.py index 61811991..ed64eed9 100644 --- a/tests/py/unit/test_material.py +++ b/tests/py/unit/test_material.py @@ -40,26 +40,25 @@ def test_basis_cell_lattice_sync(): def test_create_empty(): - """Test creating empty materials with different lattice parameters""" - # Test default parameters + """Test that creating an empty material results in empty basis""" material = Material.create_empty() assert material.basis.elements.values == [] assert material.basis.coordinates.values == [] - assert material.lattice.type == "CUB" - assert material.lattice.a == 1.0 - assert material.lattice.b == 1.0 - assert material.lattice.c == 1.0 - assert material.lattice.alpha == 90.0 - assert material.lattice.beta == 90.0 - assert material.lattice.gamma == 90.0 + + +def test_create_empty_default_params(): + """Test default parameters when creating empty material""" + material = Material.create_empty() assert material.name == "New Material" + assert material.lattice is not None + assert material.basis is not None + - # Test custom parameters +def test_create_empty_custom_lattice(): + """Test custom lattice parameters when creating empty material""" material = Material.create_empty( - a=2.0, b=3.0, c=4.0, alpha=80.0, beta=85.0, gamma=95.0, lattice_type="TRI", name="Custom Empty" + a=2.0, b=3.0, c=4.0, alpha=80.0, beta=85.0, gamma=95.0, lattice_type="TRI" ) - assert material.basis.elements.values == [] - assert material.basis.coordinates.values == [] assert material.lattice.type == "TRI" assert material.lattice.a == 2.0 assert material.lattice.b == 3.0 @@ -67,13 +66,6 @@ def test_create_empty(): assert material.lattice.alpha == 80.0 assert material.lattice.beta == 85.0 assert material.lattice.gamma == 95.0 - assert material.name == "Custom Empty" - - # Test with only 'a' parameter - material = Material.create_empty(a=2.5) - assert material.lattice.a == 2.5 - assert material.lattice.b == 2.5 - assert material.lattice.c == 2.5 def test_lattice_vectors_access(): From 97bc76e4788b9e583f5fc0e715289daf78510ca2 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 14 Jan 2025 19:46:38 -0800 Subject: [PATCH 19/20] update: simplify create_empty --- src/py/mat3ra/made/material.py | 42 +++++++++++++++------------------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/src/py/mat3ra/made/material.py b/src/py/mat3ra/made/material.py index 957f360d..f413d115 100644 --- a/src/py/mat3ra/made/material.py +++ b/src/py/mat3ra/made/material.py @@ -141,29 +141,23 @@ def create_empty( Returns: Material: A new empty Material instance with specified lattice """ - b = b if b is not None else a - c = c if c is not None else a - - basis_config = { - "elements": [], - "coordinates": [], - "units": AtomicCoordinateUnits.cartesian, - } - - lattice_config = { - "type": lattice_type, - "a": a, - "b": b, - "c": c, - "alpha": alpha, - "beta": beta, - "gamma": gamma, - "units": { - "length": Units.angstrom, - "angle": Units.degree, + empty_config = { + **cls.default_config, + "name": name, + "basis": { + "elements": [], + "coordinates": [], + "units": AtomicCoordinateUnits.cartesian, + }, + "lattice": { + "type": lattice_type, + "a": a, + "b": b if b is not None else a, + "c": c if c is not None else a, + "alpha": alpha, + "beta": beta, + "gamma": gamma, + "units": cls.default_config["lattice"]["units"], }, } - - config = {"name": name, "basis": basis_config, "lattice": lattice_config} - - return cls(config) + return cls.create(empty_config) From 9751c7972dbc7875d6d77f7278454c63da03b48f Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 14 Jan 2025 19:52:03 -0800 Subject: [PATCH 20/20] update: separate lattice tests: --- tests/py/unit/test_lattice.py | 31 +++++++++++++++++++++++++++++++ tests/py/unit/test_material.py | 29 ----------------------------- 2 files changed, 31 insertions(+), 29 deletions(-) create mode 100644 tests/py/unit/test_lattice.py diff --git a/tests/py/unit/test_lattice.py b/tests/py/unit/test_lattice.py new file mode 100644 index 00000000..5810966b --- /dev/null +++ b/tests/py/unit/test_lattice.py @@ -0,0 +1,31 @@ +from mat3ra.made.lattice import Lattice +from mat3ra.made.material import Material + +def test_create_empty_custom_lattice(): + """Test custom lattice parameters when creating empty material""" + material = Material.create_empty( + a=2.0, b=3.0, c=4.0, alpha=80.0, beta=85.0, gamma=95.0, lattice_type="TRI" + ) + assert material.lattice.type == "TRI" + assert material.lattice.a == 2.0 + assert material.lattice.b == 3.0 + assert material.lattice.c == 4.0 + assert material.lattice.alpha == 80.0 + assert material.lattice.beta == 85.0 + assert material.lattice.gamma == 95.0 + + +def test_lattice_vectors_access(): + lattice = Lattice(a=2.0, b=3.0, c=4.0) + + # Test individual vector access + assert isinstance(lattice.vectors.a, list) + assert isinstance(lattice.vectors.b, list) + assert isinstance(lattice.vectors.c, list) + + # Test vector arrays access + arrays = lattice.vector_arrays + assert len(arrays) == 3 + assert arrays[0] == lattice.vectors.a + assert arrays[1] == lattice.vectors.b + assert arrays[2] == lattice.vectors.c diff --git a/tests/py/unit/test_material.py b/tests/py/unit/test_material.py index ed64eed9..f18d0acc 100644 --- a/tests/py/unit/test_material.py +++ b/tests/py/unit/test_material.py @@ -53,32 +53,3 @@ def test_create_empty_default_params(): assert material.lattice is not None assert material.basis is not None - -def test_create_empty_custom_lattice(): - """Test custom lattice parameters when creating empty material""" - material = Material.create_empty( - a=2.0, b=3.0, c=4.0, alpha=80.0, beta=85.0, gamma=95.0, lattice_type="TRI" - ) - assert material.lattice.type == "TRI" - assert material.lattice.a == 2.0 - assert material.lattice.b == 3.0 - assert material.lattice.c == 4.0 - assert material.lattice.alpha == 80.0 - assert material.lattice.beta == 85.0 - assert material.lattice.gamma == 95.0 - - -def test_lattice_vectors_access(): - lattice = Lattice(a=2.0, b=3.0, c=4.0) - - # Test individual vector access - assert isinstance(lattice.vectors.a, list) - assert isinstance(lattice.vectors.b, list) - assert isinstance(lattice.vectors.c, list) - - # Test vector arrays access - arrays = lattice.vector_arrays - assert len(arrays) == 3 - assert arrays[0] == lattice.vectors.a - assert arrays[1] == lattice.vectors.b - assert arrays[2] == lattice.vectors.c