From 9dbff87d2f484749fa11ab5bbc28ec0ffd9a3cfc Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:42:09 -0800 Subject: [PATCH 01/14] feat: add nanoparticle builder (gpt-4o) --- .../made/tools/build/nanoparticle/__init__.py | 9 +++ .../made/tools/build/nanoparticle/builders.py | 67 +++++++++++++++++++ .../tools/build/nanoparticle/configuration.py | 49 ++++++++++++++ .../made/tools/build/nanoparticle/enums.py | 16 +++++ 4 files changed, 141 insertions(+) create mode 100644 src/py/mat3ra/made/tools/build/nanoparticle/__init__.py create mode 100644 src/py/mat3ra/made/tools/build/nanoparticle/builders.py create mode 100644 src/py/mat3ra/made/tools/build/nanoparticle/configuration.py create mode 100644 src/py/mat3ra/made/tools/build/nanoparticle/enums.py diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py b/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py new file mode 100644 index 00000000..030ecb6e --- /dev/null +++ b/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py @@ -0,0 +1,9 @@ +from mat3ra.made.material import Material + +from .builders import NanoparticleBuilder +from .configuration import NanoparticleConfiguration + + +def create_nanoparticle(configuration: NanoparticleConfiguration) -> Material: + builder = NanoparticleBuilder() + return builder.get_material(configuration) diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py new file mode 100644 index 00000000..770832f9 --- /dev/null +++ b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py @@ -0,0 +1,67 @@ +from typing import List +from ase.cluster import ( + SimpleCubic, + BodyCenteredCubic, + FaceCenteredCubic, + Icosahedron, + Octahedron, + Decahedron, + HexagonalClosedPacked, +) +from ase.cluster.wulff import wulff_construction +from mat3ra.made.material import Material +from mat3ra.made.tools.build import BaseBuilder +from mat3ra.made.tools.convert import from_ase + +from .configuration import NanoparticleConfiguration +from .enums import NanoparticleShapes + + +class NanoparticleBuilder(BaseBuilder): + """ + Generalized builder for creating nanoparticles based on ASE cluster tools. + Passes configuration parameters directly to the ASE constructors. + """ + + _ConfigurationType: type(NanoparticleConfiguration) = NanoparticleConfiguration # type: ignore + _GeneratedItemType: Material = Material + + def create_nanoparticle(self, config: NanoparticleConfiguration) -> Material: + shape = config.shape + element = config.element + + lattice_constant = config.lattice_constant + + # Ensure parameters dictionary exists + parameters = config.parameters or {} + + # Add common parameters + parameters["symbol"] = element + if "latticeconstant" not in parameters: + parameters["latticeconstant"] = lattice_constant + + # Shape-specific factory logic + if shape == NanoparticleShapes.CUBOCTAHEDRON: + nanoparticle = FaceCenteredCubic(**parameters) + elif shape == NanoparticleShapes.ICOSAHEDRON: + nanoparticle = Icosahedron(**parameters) + elif shape == NanoparticleShapes.OCTAHEDRON: + nanoparticle = Octahedron(**parameters) + elif shape == NanoparticleShapes.DECAHEDRON: + nanoparticle = Decahedron(**parameters) + elif shape == NanoparticleShapes.SIMPLE_CUBIC: + nanoparticle = SimpleCubic(**parameters) + elif shape == NanoparticleShapes.BODY_CENTERED_CUBIC: + nanoparticle = BodyCenteredCubic(**parameters) + elif shape == NanoparticleShapes.HEXAGONAL_CLOSED_PACKED: + nanoparticle = HexagonalClosedPacked(**parameters) + elif shape == NanoparticleShapes.WULFF: + nanoparticle = wulff_construction(**parameters) + else: + raise ValueError(f"Unsupported shape: {shape}") + + return Material(from_ase(nanoparticle)) + + def _generate(self, configuration: NanoparticleConfiguration) -> List[_GeneratedItemType]: + nanoparticle = self.create_nanoparticle(configuration) + return [nanoparticle] diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py b/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py new file mode 100644 index 00000000..43b1dd87 --- /dev/null +++ b/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py @@ -0,0 +1,49 @@ +from typing import Dict, Optional + +import numpy as np +from mat3ra.made.material import Material +from .enums import NanoparticleShapes +from ...build import BaseConfiguration + + +class NanoparticleConfiguration(BaseConfiguration): + """ + Configuration for building a nanoparticle. + + Attributes: + material (Material): The base material for the nanoparticle. + shape (NanoparticleShapes): The desired shape of the nanoparticle. + parameters (dict): Dictionary of parameters to pass to the corresponding ASE constructor. + vacuum_padding (float): Vacuum padding around the nanoparticle. + """ + + material: Material + shape: NanoparticleShapes + parameters: Optional[Dict] = None # Shape-specific parameters (e.g., layers, size) + vacuum_padding: float = 10.0 # Padding for vacuum space around the nanoparticle + + @property + def lattice_type(self) -> str: + return self.material.lattice.type + + @property + def lattice_constant(self) -> float: + lattice_constants = [self.material.lattice.a, self.material.lattice.b, self.material.lattice.c] + # If lattice constants are not equal within a tolerance, raise an error + if not np.all(np.isclose(lattice_constants, lattice_constants[0], atol=1e-6)): + raise ValueError("Lattice constants must be equal for isotropic materials") + lattice_constant = lattice_constants[0] + return lattice_constant + + @property + def element(self) -> str: + return self.material.basis.elements[0] + + @property + def _json(self): + return { + "material": self.material.to_json(), + "shape": self.shape.value, + "parameters": self.parameters, + "vacuum_padding": self.vacuum_padding, + } diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/enums.py b/src/py/mat3ra/made/tools/build/nanoparticle/enums.py new file mode 100644 index 00000000..bd1844b9 --- /dev/null +++ b/src/py/mat3ra/made/tools/build/nanoparticle/enums.py @@ -0,0 +1,16 @@ +from enum import Enum + + +class NanoparticleShapes(str, Enum): + """ + Enum for supported nanoparticle shapes. + """ + + CUBOCTAHEDRON = "cuboctahedron" + ICOSAHEDRON = "icosahedron" + OCTAHEDRON = "octahedron" + DECAHEDRON = "decahedron" + SIMPLE_CUBIC = "simple_cubic" + BODY_CENTERED_CUBIC = "body_centered_cubic" + HEXAGONAL_CLOSED_PACKED = "hexagonal_closed_packed" + WULFF = "wulff" From fb864b97c2f7f329c2f02f3e51c53c09924c3649 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:21:44 -0800 Subject: [PATCH 02/14] update: fix lattice conv vs primitive --- .../made/tools/build/nanoparticle/builders.py | 16 +++++---- .../tools/build/nanoparticle/configuration.py | 35 +++++++++++++++++-- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py index 770832f9..09f69cf4 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py @@ -9,24 +9,24 @@ HexagonalClosedPacked, ) from ase.cluster.wulff import wulff_construction -from mat3ra.made.material import Material from mat3ra.made.tools.build import BaseBuilder -from mat3ra.made.tools.convert import from_ase +from mat3ra.made.tools.build.mixins import ConvertGeneratedItemsASEAtomsMixin from .configuration import NanoparticleConfiguration from .enums import NanoparticleShapes +from ...third_party import ASEAtoms -class NanoparticleBuilder(BaseBuilder): +class NanoparticleBuilder(ConvertGeneratedItemsASEAtomsMixin, BaseBuilder): """ Generalized builder for creating nanoparticles based on ASE cluster tools. Passes configuration parameters directly to the ASE constructors. """ _ConfigurationType: type(NanoparticleConfiguration) = NanoparticleConfiguration # type: ignore - _GeneratedItemType: Material = Material + _GeneratedItemType: type(ASEAtoms) = ASEAtoms # type: ignore - def create_nanoparticle(self, config: NanoparticleConfiguration) -> Material: + def create_nanoparticle(self, config: NanoparticleConfiguration) -> _GeneratedItemType: shape = config.shape element = config.element @@ -59,8 +59,10 @@ def create_nanoparticle(self, config: NanoparticleConfiguration) -> Material: nanoparticle = wulff_construction(**parameters) else: raise ValueError(f"Unsupported shape: {shape}") - - return Material(from_ase(nanoparticle)) + box_size = 2 * max(abs(nanoparticle.positions).max(axis=0)) + config.vacuum_padding + nanoparticle.set_cell([box_size, box_size, box_size], scale_atoms=False) + nanoparticle.center() + return nanoparticle def _generate(self, configuration: NanoparticleConfiguration) -> List[_GeneratedItemType]: nanoparticle = self.create_nanoparticle(configuration) diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py b/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py index 43b1dd87..ac574840 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py @@ -2,6 +2,12 @@ import numpy as np from mat3ra.made.material import Material +from mat3ra.made.tools.convert import decorator_convert_material_args_kwargs_to_structure +from pymatgen.symmetry.analyzer import SpacegroupAnalyzer + +from ...convert import from_pymatgen +from ...third_party import PymatgenStructure + from .enums import NanoparticleShapes from ...build import BaseConfiguration @@ -28,7 +34,13 @@ def lattice_type(self) -> str: @property def lattice_constant(self) -> float: - lattice_constants = [self.material.lattice.a, self.material.lattice.b, self.material.lattice.c] + material = self.material + conventional_material = NanoparticleConfiguration.convert_to_conventional(material) + lattice_constants = [ + conventional_material.lattice.a, + conventional_material.lattice.b, + conventional_material.lattice.c, + ] # If lattice constants are not equal within a tolerance, raise an error if not np.all(np.isclose(lattice_constants, lattice_constants[0], atol=1e-6)): raise ValueError("Lattice constants must be equal for isotropic materials") @@ -37,7 +49,7 @@ def lattice_constant(self) -> float: @property def element(self) -> str: - return self.material.basis.elements[0] + return self.material.basis.elements.get_element_value_by_index(0) @property def _json(self): @@ -47,3 +59,22 @@ def _json(self): "parameters": self.parameters, "vacuum_padding": self.vacuum_padding, } + + # TODO: move to a separate module + @staticmethod + @decorator_convert_material_args_kwargs_to_structure + def convert_to_primitive(structure: PymatgenStructure) -> Material: + """ + Convert a structure to its primitive cell. + """ + analyzer = SpacegroupAnalyzer(structure) + return Material(from_pymatgen(analyzer.get_primitive_standard_structure())) + + @staticmethod + @decorator_convert_material_args_kwargs_to_structure + def convert_to_conventional(structure: PymatgenStructure) -> Material: + """ + Convert a structure to its conventional cell. + """ + analyzer = SpacegroupAnalyzer(structure) + return Material(from_pymatgen(analyzer.get_conventional_standard_structure())) From 5c56c04a22cf7b352010e767f2f297cbe71c30aa Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:15:00 -0800 Subject: [PATCH 03/14] update: add ase and our config --- .../tools/build/nanoparticle/configuration.py | 60 +++++++++++++------ 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py b/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py index ac574840..2e13c7a5 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional +from typing import Dict, Optional, Tuple import numpy as np from mat3ra.made.material import Material @@ -12,20 +12,8 @@ from ...build import BaseConfiguration -class NanoparticleConfiguration(BaseConfiguration): - """ - Configuration for building a nanoparticle. - - Attributes: - material (Material): The base material for the nanoparticle. - shape (NanoparticleShapes): The desired shape of the nanoparticle. - parameters (dict): Dictionary of parameters to pass to the corresponding ASE constructor. - vacuum_padding (float): Vacuum padding around the nanoparticle. - """ - - material: Material - shape: NanoparticleShapes - parameters: Optional[Dict] = None # Shape-specific parameters (e.g., layers, size) +class BaseNanoparticleConfiguration(BaseConfiguration): + material: Material # Material to use for the nanoparticle vacuum_padding: float = 10.0 # Padding for vacuum space around the nanoparticle @property @@ -35,7 +23,7 @@ def lattice_type(self) -> str: @property def lattice_constant(self) -> float: material = self.material - conventional_material = NanoparticleConfiguration.convert_to_conventional(material) + conventional_material = ASENanoparticleConfiguration.convert_to_conventional(material) lattice_constants = [ conventional_material.lattice.a, conventional_material.lattice.b, @@ -55,8 +43,6 @@ def element(self) -> str: def _json(self): return { "material": self.material.to_json(), - "shape": self.shape.value, - "parameters": self.parameters, "vacuum_padding": self.vacuum_padding, } @@ -78,3 +64,41 @@ def convert_to_conventional(structure: PymatgenStructure) -> Material: """ analyzer = SpacegroupAnalyzer(structure) return Material(from_pymatgen(analyzer.get_conventional_standard_structure())) + + +class NanoparticleConfiguration(BaseNanoparticleConfiguration): + shape: NanoparticleShapes = NanoparticleShapes.ICOSAHEDRON # Shape of the nanoparticle + orientation_z: Tuple[int, int, int] = (0, 0, 1) # Orientation of the crystallographic axis in the z-direction + radius: float = 5.0 # Radius of the nanoparticle (largest feature size for a shape), in Angstroms + + @property + def _json(self): + return { + **super()._json, + "shape": self.shape.value, + "orientation_z": self.orientation_z, + "radius": self.radius, + } + + +class ASENanoparticleConfiguration(BaseNanoparticleConfiguration): + """ + Configuration for building a nanoparticle. + + Attributes: + material (Material): The base material for the nanoparticle. + shape (NanoparticleShapes): The desired shape of the nanoparticle. + parameters (dict): Dictionary of parameters to pass to the corresponding ASE constructor. + vacuum_padding (float): Vacuum padding around the nanoparticle. + """ + + shape: NanoparticleShapes + parameters: Optional[Dict] = None # Shape-specific parameters (e.g., layers, size) + + @property + def _json(self): + return { + **super()._json, + "shape": self.shape.value, + "parameters": self.parameters, + } From 4de1a60e365201180a5a68bd0302bf5e1a6c552a Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:16:31 -0800 Subject: [PATCH 04/14] update: add cutting nanoparticle builder --- .../made/tools/build/nanoparticle/builders.py | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py index 09f69cf4..f85ed111 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py @@ -9,24 +9,59 @@ HexagonalClosedPacked, ) from ase.cluster.wulff import wulff_construction +from mat3ra.made.material import Material from mat3ra.made.tools.build import BaseBuilder from mat3ra.made.tools.build.mixins import ConvertGeneratedItemsASEAtomsMixin +from mat3ra.made.tools.build.slab import SlabConfiguration +from mat3ra.made.tools.modify import filter_by_condition_on_coordinates -from .configuration import NanoparticleConfiguration +from .configuration import ASENanoparticleConfiguration, NanoparticleConfiguration from .enums import NanoparticleShapes +from ..slab import create_slab from ...third_party import ASEAtoms class NanoparticleBuilder(ConvertGeneratedItemsASEAtomsMixin, BaseBuilder): + """ + Builder for creating nanoparticles by cutting from bulk materials supercells. + """ + + _ConfigurationType: type(NanoparticleConfiguration) = NanoparticleConfiguration # type: ignore + _GeneratedItemType: type(Material) = Material # type: ignore + + def create_nanoparticle(self, config: _ConfigurationType) -> _GeneratedItemType: + material = config.material + # shape = config.shape + orientation_z = config.orientation_z + radius = config.radius + + # Get the conventional structure + conventional_material = self._ConfigurationType.convert_to_conventional(material) + thickness_in_layers = (2 * radius + config.vacuum_padding) / conventional_material.lattice_constant + slab_config = SlabConfiguration( + bulk=conventional_material.structure, + miller_indices=orientation_z, + thickness=thickness_in_layers, + use_conventional_cell=True, + use_orthogonal_z=True, + make_primitive=False, + ) + slab = create_slab(slab_config) + # TODO: switch condition based on shape + nanoparticle = filter_by_condition_on_coordinates(slab, lambda vector: vector.norm() <= radius) + return nanoparticle + + +class ASEBasedNanoparticleBuilder(ConvertGeneratedItemsASEAtomsMixin, NanoparticleBuilder): """ Generalized builder for creating nanoparticles based on ASE cluster tools. Passes configuration parameters directly to the ASE constructors. """ - _ConfigurationType: type(NanoparticleConfiguration) = NanoparticleConfiguration # type: ignore + _ConfigurationType: type(ASENanoparticleConfiguration) = ASENanoparticleConfiguration # type: ignore _GeneratedItemType: type(ASEAtoms) = ASEAtoms # type: ignore - def create_nanoparticle(self, config: NanoparticleConfiguration) -> _GeneratedItemType: + def create_nanoparticle(self, config: ASENanoparticleConfiguration) -> _GeneratedItemType: shape = config.shape element = config.element @@ -39,7 +74,7 @@ def create_nanoparticle(self, config: NanoparticleConfiguration) -> _GeneratedIt parameters["symbol"] = element if "latticeconstant" not in parameters: parameters["latticeconstant"] = lattice_constant - + # TODO: adjust parameters for octahedron based on type (cuboctahedron, regular octahedron, etc.) # Shape-specific factory logic if shape == NanoparticleShapes.CUBOCTAHEDRON: nanoparticle = FaceCenteredCubic(**parameters) @@ -64,6 +99,6 @@ def create_nanoparticle(self, config: NanoparticleConfiguration) -> _GeneratedIt nanoparticle.center() return nanoparticle - def _generate(self, configuration: NanoparticleConfiguration) -> List[_GeneratedItemType]: + def _generate(self, configuration: ASENanoparticleConfiguration) -> List[_GeneratedItemType]: nanoparticle = self.create_nanoparticle(configuration) return [nanoparticle] From 045a0d02e09a8b881a03142ba20df47f9da57db8 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:16:44 -0800 Subject: [PATCH 05/14] update: rename to ase builder --- src/py/mat3ra/made/tools/build/nanoparticle/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py b/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py index 030ecb6e..346e8ccf 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py @@ -1,9 +1,9 @@ from mat3ra.made.material import Material from .builders import NanoparticleBuilder -from .configuration import NanoparticleConfiguration +from .configuration import ASENanoparticleConfiguration -def create_nanoparticle(configuration: NanoparticleConfiguration) -> Material: +def create_nanoparticle(configuration: ASENanoparticleConfiguration) -> Material: builder = NanoparticleBuilder() return builder.get_material(configuration) From 0e975937ce51b581de6e625d8b11c1786e4b12e3 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 15 Dec 2024 19:53:26 -0800 Subject: [PATCH 06/14] update: use correct builders --- .../made/tools/build/nanoparticle/__init__.py | 14 +++++++++----- .../made/tools/build/nanoparticle/builders.py | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py b/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py index 346e8ccf..56226df8 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py @@ -1,9 +1,13 @@ +from typing import Union + from mat3ra.made.material import Material -from .builders import NanoparticleBuilder -from .configuration import ASENanoparticleConfiguration +from .builders import NanoparticleBuilder, ASEBasedNanoparticleBuilder +from .configuration import ASENanoparticleConfiguration, NanoparticleConfiguration -def create_nanoparticle(configuration: ASENanoparticleConfiguration) -> Material: - builder = NanoparticleBuilder() - return builder.get_material(configuration) +def create_nanoparticle( + configuration: Union[NanoparticleConfiguration, ASENanoparticleConfiguration], + builder: Union[NanoparticleBuilder, ASEBasedNanoparticleBuilder] = NanoparticleBuilder(), +) -> Material: + return builder.create_nanoparticle(configuration) diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py index f85ed111..a10449cd 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py @@ -21,7 +21,7 @@ from ...third_party import ASEAtoms -class NanoparticleBuilder(ConvertGeneratedItemsASEAtomsMixin, BaseBuilder): +class NanoparticleBuilder(BaseBuilder): """ Builder for creating nanoparticles by cutting from bulk materials supercells. """ From cb36e439f962869f97426e13683e610396104f94 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Sun, 15 Dec 2024 19:54:20 -0800 Subject: [PATCH 07/14] fix: return materil from builder --- src/py/mat3ra/made/tools/build/nanoparticle/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py b/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py index 56226df8..8301760f 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py @@ -10,4 +10,4 @@ def create_nanoparticle( configuration: Union[NanoparticleConfiguration, ASENanoparticleConfiguration], builder: Union[NanoparticleBuilder, ASEBasedNanoparticleBuilder] = NanoparticleBuilder(), ) -> Material: - return builder.create_nanoparticle(configuration) + return builder.get_material(configuration) From 6c9c9af83d38067c2b2588423957a366611c40f8 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:48:02 -0800 Subject: [PATCH 08/14] update: simplify and isolate --- .../made/tools/build/nanoparticle/__init__.py | 4 +- .../made/tools/build/nanoparticle/builders.py | 70 +++++++++---------- .../tools/build/nanoparticle/configuration.py | 4 +- .../made/tools/build/nanoparticle/enums.py | 2 +- 4 files changed, 39 insertions(+), 41 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py b/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py index 8301760f..62b8259a 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py @@ -3,11 +3,11 @@ from mat3ra.made.material import Material from .builders import NanoparticleBuilder, ASEBasedNanoparticleBuilder -from .configuration import ASENanoparticleConfiguration, NanoparticleConfiguration +from .configuration import ASEBasedNanoparticleConfiguration, NanoparticleConfiguration def create_nanoparticle( - configuration: Union[NanoparticleConfiguration, ASENanoparticleConfiguration], + configuration: Union[NanoparticleConfiguration, ASEBasedNanoparticleConfiguration], builder: Union[NanoparticleBuilder, ASEBasedNanoparticleBuilder] = NanoparticleBuilder(), ) -> Material: return builder.get_material(configuration) diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py index a10449cd..19b4109d 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Callable, Dict from ase.cluster import ( SimpleCubic, BodyCenteredCubic, @@ -10,16 +10,28 @@ ) from ase.cluster.wulff import wulff_construction from mat3ra.made.material import Material +from mat3ra.made.tools.analyze.other import get_chemical_formula from mat3ra.made.tools.build import BaseBuilder from mat3ra.made.tools.build.mixins import ConvertGeneratedItemsASEAtomsMixin from mat3ra.made.tools.build.slab import SlabConfiguration from mat3ra.made.tools.modify import filter_by_condition_on_coordinates -from .configuration import ASENanoparticleConfiguration, NanoparticleConfiguration +from .configuration import ASEBasedNanoparticleConfiguration, NanoparticleConfiguration from .enums import NanoparticleShapes from ..slab import create_slab from ...third_party import ASEAtoms +SHAPE_TO_CONSTRUCTOR: Dict[str, Callable[..., ASEAtoms]] = { + NanoparticleShapes.ICOSAHEDRON: Icosahedron, + NanoparticleShapes.OCTAHEDRON: Octahedron, + NanoparticleShapes.DECAHEDRON: Decahedron, + NanoparticleShapes.SIMPLE_CUBIC: SimpleCubic, + NanoparticleShapes.FACE_CENTERED_CUBIC: FaceCenteredCubic, + NanoparticleShapes.BODY_CENTERED_CUBIC: BodyCenteredCubic, + NanoparticleShapes.HEXAGONAL_CLOSED_PACKED: HexagonalClosedPacked, + NanoparticleShapes.WULFF: wulff_construction, +} + class NanoparticleBuilder(BaseBuilder): """ @@ -51,6 +63,11 @@ def create_nanoparticle(self, config: _ConfigurationType) -> _GeneratedItemType: nanoparticle = filter_by_condition_on_coordinates(slab, lambda vector: vector.norm() <= radius) return nanoparticle + def _finalize(self, materials: List[Material], configuration: _ConfigurationType) -> List[Material]: + for material in materials: + material.name = f"{get_chemical_formula(material)} {configuration.shape.value.capitalize()}" + return materials + class ASEBasedNanoparticleBuilder(ConvertGeneratedItemsASEAtomsMixin, NanoparticleBuilder): """ @@ -58,47 +75,28 @@ class ASEBasedNanoparticleBuilder(ConvertGeneratedItemsASEAtomsMixin, Nanopartic Passes configuration parameters directly to the ASE constructors. """ - _ConfigurationType: type(ASENanoparticleConfiguration) = ASENanoparticleConfiguration # type: ignore + _ConfigurationType: type(ASEBasedNanoparticleConfiguration) = ASEBasedNanoparticleConfiguration # type: ignore _GeneratedItemType: type(ASEAtoms) = ASEAtoms # type: ignore - def create_nanoparticle(self, config: ASENanoparticleConfiguration) -> _GeneratedItemType: - shape = config.shape - element = config.element - - lattice_constant = config.lattice_constant - - # Ensure parameters dictionary exists + def create_nanoparticle(self, config: ASEBasedNanoparticleConfiguration) -> _GeneratedItemType: + constructor = SHAPE_TO_CONSTRUCTOR.get(config.shape.value) + if not constructor: + raise ValueError(f"Unsupported shape: {config.shape}") parameters = config.parameters or {} - - # Add common parameters - parameters["symbol"] = element - if "latticeconstant" not in parameters: - parameters["latticeconstant"] = lattice_constant - # TODO: adjust parameters for octahedron based on type (cuboctahedron, regular octahedron, etc.) - # Shape-specific factory logic - if shape == NanoparticleShapes.CUBOCTAHEDRON: - nanoparticle = FaceCenteredCubic(**parameters) - elif shape == NanoparticleShapes.ICOSAHEDRON: - nanoparticle = Icosahedron(**parameters) - elif shape == NanoparticleShapes.OCTAHEDRON: - nanoparticle = Octahedron(**parameters) - elif shape == NanoparticleShapes.DECAHEDRON: - nanoparticle = Decahedron(**parameters) - elif shape == NanoparticleShapes.SIMPLE_CUBIC: - nanoparticle = SimpleCubic(**parameters) - elif shape == NanoparticleShapes.BODY_CENTERED_CUBIC: - nanoparticle = BodyCenteredCubic(**parameters) - elif shape == NanoparticleShapes.HEXAGONAL_CLOSED_PACKED: - nanoparticle = HexagonalClosedPacked(**parameters) - elif shape == NanoparticleShapes.WULFF: - nanoparticle = wulff_construction(**parameters) - else: - raise ValueError(f"Unsupported shape: {shape}") + parameters["symbol"] = config.element + parameters.setdefault("latticeconstant", config.lattice_constant) + nanoparticle = constructor(**parameters) box_size = 2 * max(abs(nanoparticle.positions).max(axis=0)) + config.vacuum_padding nanoparticle.set_cell([box_size, box_size, box_size], scale_atoms=False) nanoparticle.center() + return nanoparticle - def _generate(self, configuration: ASENanoparticleConfiguration) -> List[_GeneratedItemType]: + def _generate(self, configuration: ASEBasedNanoparticleConfiguration) -> List[_GeneratedItemType]: nanoparticle = self.create_nanoparticle(configuration) return [nanoparticle] + + def _finalize(self, materials: List[Material], configuration: _ConfigurationType) -> List[Material]: + for material in materials: + material.name = f"{get_chemical_formula(material)} {configuration.shape.value.capitalize()}" + return materials diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py b/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py index 2e13c7a5..f3c5c19e 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py @@ -23,7 +23,7 @@ def lattice_type(self) -> str: @property def lattice_constant(self) -> float: material = self.material - conventional_material = ASENanoparticleConfiguration.convert_to_conventional(material) + conventional_material = ASEBasedNanoparticleConfiguration.convert_to_conventional(material) lattice_constants = [ conventional_material.lattice.a, conventional_material.lattice.b, @@ -81,7 +81,7 @@ def _json(self): } -class ASENanoparticleConfiguration(BaseNanoparticleConfiguration): +class ASEBasedNanoparticleConfiguration(BaseNanoparticleConfiguration): """ Configuration for building a nanoparticle. diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/enums.py b/src/py/mat3ra/made/tools/build/nanoparticle/enums.py index bd1844b9..820b30a0 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/enums.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/enums.py @@ -6,11 +6,11 @@ class NanoparticleShapes(str, Enum): Enum for supported nanoparticle shapes. """ - CUBOCTAHEDRON = "cuboctahedron" ICOSAHEDRON = "icosahedron" OCTAHEDRON = "octahedron" DECAHEDRON = "decahedron" SIMPLE_CUBIC = "simple_cubic" + FACE_CENTERED_CUBIC = "face_centered_cubic" BODY_CENTERED_CUBIC = "body_centered_cubic" HEXAGONAL_CLOSED_PACKED = "hexagonal_closed_packed" WULFF = "wulff" From c605bebaff03c59344ff6edd07c756cab4521199 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:09:44 -0800 Subject: [PATCH 09/14] update: switch condition based on shape --- .../made/tools/build/nanoparticle/builders.py | 17 +++++++++++++++-- .../made/tools/build/nanoparticle/enums.py | 1 + 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py index 19b4109d..4377651f 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py @@ -15,10 +15,12 @@ from mat3ra.made.tools.build.mixins import ConvertGeneratedItemsASEAtomsMixin from mat3ra.made.tools.build.slab import SlabConfiguration from mat3ra.made.tools.modify import filter_by_condition_on_coordinates +from mat3ra.made.tools.utils.coordinate import SphereCoordinateCondition from .configuration import ASEBasedNanoparticleConfiguration, NanoparticleConfiguration from .enums import NanoparticleShapes from ..slab import create_slab +from ...analyze.other import get_closest_site_id_from_coordinate from ...third_party import ASEAtoms SHAPE_TO_CONSTRUCTOR: Dict[str, Callable[..., ASEAtoms]] = { @@ -59,10 +61,21 @@ def create_nanoparticle(self, config: _ConfigurationType) -> _GeneratedItemType: make_primitive=False, ) slab = create_slab(slab_config) - # TODO: switch condition based on shape - nanoparticle = filter_by_condition_on_coordinates(slab, lambda vector: vector.norm() <= radius) + slab.to_cartesian() + center_coordinate = slab.basis.cell.convert_point_to_cartesian([0.5, 0.5, 0.5]) + center_id_at_site = get_closest_site_id_from_coordinate(slab, center_coordinate, use_cartesian_coordinates=True) + center_coordinate_at_site = slab.basis.coordinates.get_element_value_by_index(center_id_at_site) + condition = self._get_condition_by_shape(config.shape, center_coordinate_at_site, radius) + nanoparticle = filter_by_condition_on_coordinates(slab, condition, use_cartesian_coordinates=True) return nanoparticle + def _get_condition_by_shape( + self, shape: NanoparticleShapes, center_coordinate: List[float], radius: float + ) -> Callable[[List[float]], bool]: + if shape == NanoparticleShapes.SPHERE: + return SphereCoordinateCondition(center_position=center_coordinate, radius=radius).condition + raise ValueError(f"Unsupported shape: {shape}") + def _finalize(self, materials: List[Material], configuration: _ConfigurationType) -> List[Material]: for material in materials: material.name = f"{get_chemical_formula(material)} {configuration.shape.value.capitalize()}" diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/enums.py b/src/py/mat3ra/made/tools/build/nanoparticle/enums.py index 820b30a0..a53b1206 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/enums.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/enums.py @@ -6,6 +6,7 @@ class NanoparticleShapes(str, Enum): Enum for supported nanoparticle shapes. """ + SPHERE = "sphere" ICOSAHEDRON = "icosahedron" OCTAHEDRON = "octahedron" DECAHEDRON = "decahedron" From e4310957486741e8043dbdc357f0abaea2a2159c Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:43:56 -0800 Subject: [PATCH 10/14] update: create more builders --- src/py/mat3ra/made/tools/analyze/lattice.py | 0 .../made/tools/build/nanoparticle/builders.py | 123 ++++++++++-------- 2 files changed, 66 insertions(+), 57 deletions(-) create mode 100644 src/py/mat3ra/made/tools/analyze/lattice.py diff --git a/src/py/mat3ra/made/tools/analyze/lattice.py b/src/py/mat3ra/made/tools/analyze/lattice.py new file mode 100644 index 00000000..e69de29b diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py index 4377651f..24a06e98 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py @@ -1,14 +1,5 @@ from typing import List, Callable, Dict -from ase.cluster import ( - SimpleCubic, - BodyCenteredCubic, - FaceCenteredCubic, - Icosahedron, - Octahedron, - Decahedron, - HexagonalClosedPacked, -) -from ase.cluster.wulff import wulff_construction + from mat3ra.made.material import Material from mat3ra.made.tools.analyze.other import get_chemical_formula from mat3ra.made.tools.build import BaseBuilder @@ -17,72 +8,73 @@ from mat3ra.made.tools.modify import filter_by_condition_on_coordinates from mat3ra.made.tools.utils.coordinate import SphereCoordinateCondition -from .configuration import ASEBasedNanoparticleConfiguration, NanoparticleConfiguration -from .enums import NanoparticleShapes +from .configuration import ASEBasedNanoparticleConfiguration, SphereSlabBasedNanoparticleConfiguration +from .enums import ASENanoparticleShapesEnum from ..slab import create_slab from ...analyze.other import get_closest_site_id_from_coordinate from ...third_party import ASEAtoms -SHAPE_TO_CONSTRUCTOR: Dict[str, Callable[..., ASEAtoms]] = { - NanoparticleShapes.ICOSAHEDRON: Icosahedron, - NanoparticleShapes.OCTAHEDRON: Octahedron, - NanoparticleShapes.DECAHEDRON: Decahedron, - NanoparticleShapes.SIMPLE_CUBIC: SimpleCubic, - NanoparticleShapes.FACE_CENTERED_CUBIC: FaceCenteredCubic, - NanoparticleShapes.BODY_CENTERED_CUBIC: BodyCenteredCubic, - NanoparticleShapes.HEXAGONAL_CLOSED_PACKED: HexagonalClosedPacked, - NanoparticleShapes.WULFF: wulff_construction, -} - -class NanoparticleBuilder(BaseBuilder): +class SlabBasedNanoparticleBuilder(BaseBuilder): """ Builder for creating nanoparticles by cutting from bulk materials supercells. """ - _ConfigurationType: type(NanoparticleConfiguration) = NanoparticleConfiguration # type: ignore + _ConfigurationType: type(ASEBasedNanoparticleConfiguration) = ASEBasedNanoparticleConfiguration # type: ignore _GeneratedItemType: type(Material) = Material # type: ignore def create_nanoparticle(self, config: _ConfigurationType) -> _GeneratedItemType: - material = config.material - # shape = config.shape - orientation_z = config.orientation_z - radius = config.radius - - # Get the conventional structure - conventional_material = self._ConfigurationType.convert_to_conventional(material) - thickness_in_layers = (2 * radius + config.vacuum_padding) / conventional_material.lattice_constant + slab = self._create_slab(config) + center_coordinate = self._find_slab_center_coordinate(slab) + condition = self._build_coordinate_condition(config, center_coordinate) + nanoparticle = filter_by_condition_on_coordinates(slab, condition, use_cartesian_coordinates=True) + return nanoparticle + + def _build_coordinate_condition(self, config: _ConfigurationType, center_coordinate: List[float]) -> Callable: + coordinate_condition = config.condition_builder(center_coordinate) + return coordinate_condition.condition + + def _create_slab(self, config: _ConfigurationType) -> Material: slab_config = SlabConfiguration( - bulk=conventional_material.structure, - miller_indices=orientation_z, - thickness=thickness_in_layers, + bulk=config.material, + miller_indices=config.orientation_z, + thickness=config.supercell_size, use_conventional_cell=True, use_orthogonal_z=True, make_primitive=False, + vacuum=0, + xy_supercell_matrix=[[config.supercell_size, 0], [0, config.supercell_size]], ) slab = create_slab(slab_config) + return slab + + def _find_slab_center_coordinate(self, slab: Material) -> List[float]: slab.to_cartesian() center_coordinate = slab.basis.cell.convert_point_to_cartesian([0.5, 0.5, 0.5]) center_id_at_site = get_closest_site_id_from_coordinate(slab, center_coordinate, use_cartesian_coordinates=True) center_coordinate_at_site = slab.basis.coordinates.get_element_value_by_index(center_id_at_site) - condition = self._get_condition_by_shape(config.shape, center_coordinate_at_site, radius) - nanoparticle = filter_by_condition_on_coordinates(slab, condition, use_cartesian_coordinates=True) - return nanoparticle - - def _get_condition_by_shape( - self, shape: NanoparticleShapes, center_coordinate: List[float], radius: float - ) -> Callable[[List[float]], bool]: - if shape == NanoparticleShapes.SPHERE: - return SphereCoordinateCondition(center_position=center_coordinate, radius=radius).condition - raise ValueError(f"Unsupported shape: {shape}") + return center_coordinate_at_site def _finalize(self, materials: List[Material], configuration: _ConfigurationType) -> List[Material]: for material in materials: - material.name = f"{get_chemical_formula(material)} {configuration.shape.value.capitalize()}" + material.name = f"{get_chemical_formula(material)} Nanoparticle" return materials -class ASEBasedNanoparticleBuilder(ConvertGeneratedItemsASEAtomsMixin, NanoparticleBuilder): +class SphereSlabBasedNanoparticleBuilder(SlabBasedNanoparticleBuilder): + """ + Builder for creating spherical nanoparticles by cutting from bulk materials supercells. + """ + + _ConfigurationType: type(SphereSlabBasedNanoparticleConfiguration) = ( # type: ignore + SphereSlabBasedNanoparticleConfiguration + ) + + def _build_coordinate_condition(self, config: _ConfigurationType, center_coordinate: List[float]) -> Callable: + return SphereCoordinateCondition(center_position=center_coordinate, radius=config.radius).condition + + +class ASEBasedNanoparticleBuilder(ConvertGeneratedItemsASEAtomsMixin, BaseBuilder): """ Generalized builder for creating nanoparticles based on ASE cluster tools. Passes configuration parameters directly to the ASE constructors. @@ -92,18 +84,35 @@ class ASEBasedNanoparticleBuilder(ConvertGeneratedItemsASEAtomsMixin, Nanopartic _GeneratedItemType: type(ASEAtoms) = ASEAtoms # type: ignore def create_nanoparticle(self, config: ASEBasedNanoparticleConfiguration) -> _GeneratedItemType: - constructor = SHAPE_TO_CONSTRUCTOR.get(config.shape.value) - if not constructor: - raise ValueError(f"Unsupported shape: {config.shape}") + parameters = self._get_ase_nanoparticle_parameters(config) + constructor = self._get_ase_nanoparticle_constructor(config) + nanoparticle_without_cell = constructor(**parameters) + nanoparticle = self._set_ase_cell(nanoparticle_without_cell, config.vacuum_padding) + + return nanoparticle + + @staticmethod + def _get_ase_nanoparticle_parameters(config: ASEBasedNanoparticleConfiguration) -> Dict: parameters = config.parameters or {} parameters["symbol"] = config.element parameters.setdefault("latticeconstant", config.lattice_constant) - nanoparticle = constructor(**parameters) - box_size = 2 * max(abs(nanoparticle.positions).max(axis=0)) + config.vacuum_padding - nanoparticle.set_cell([box_size, box_size, box_size], scale_atoms=False) - nanoparticle.center() - - return nanoparticle + return parameters + + @classmethod + def _get_ase_nanoparticle_constructor(cls, config: ASEBasedNanoparticleConfiguration) -> Callable[..., ASEAtoms]: + constructor = ASENanoparticleShapesEnum.get_ase_constructor(config.shape.value) + return constructor + + @staticmethod + def _set_ase_cell(atoms: ASEAtoms, vacuum: float) -> _GeneratedItemType: + """ + Set the cell of an ASE atoms object to a cubic box with vacuum padding around the nanoparticle. + """ + max_dimension_along_x = abs(atoms.positions[:, 0]).max() + box_size = 2 * max_dimension_along_x + vacuum + atoms.set_cell([box_size, box_size, box_size], scale_atoms=False) + atoms.center() + return atoms def _generate(self, configuration: ASEBasedNanoparticleConfiguration) -> List[_GeneratedItemType]: nanoparticle = self.create_nanoparticle(configuration) From 930d9c615a76922ab6299cd18776208ca3bb4d8a Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:44:15 -0800 Subject: [PATCH 11/14] update: create builders and configs for nanoparticles --- src/py/mat3ra/made/tools/analyze/lattice.py | 27 +++++++++ .../made/tools/build/nanoparticle/__init__.py | 14 +++-- .../made/tools/build/nanoparticle/builders.py | 4 +- .../tools/build/nanoparticle/configuration.py | 55 +++++++------------ .../made/tools/build/nanoparticle/enums.py | 32 ++++++++++- 5 files changed, 89 insertions(+), 43 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze/lattice.py b/src/py/mat3ra/made/tools/analyze/lattice.py index e69de29b..45c26580 100644 --- a/src/py/mat3ra/made/tools/analyze/lattice.py +++ b/src/py/mat3ra/made/tools/analyze/lattice.py @@ -0,0 +1,27 @@ +from mat3ra.made.material import Material +from mat3ra.made.tools.analyze import BaseMaterialAnalyzer +from mat3ra.made.tools.convert import from_pymatgen +from ..third_party import PymatgenSpacegroupAnalyzer + +from mat3ra.made.tools.convert import to_pymatgen + + +class LatticeMaterialAnalyzer(BaseMaterialAnalyzer): + + def __init__(self, material: Material): + super().__init__(material) + self.spacegroup_analyzer = PymatgenSpacegroupAnalyzer(to_pymatgen(self.material)) + + @property + def get_with_primitive_lattice(self: Material) -> Material: + """ + Convert a structure to its primitive cell. + """ + return Material(from_pymatgen(self.spacegroup_analyzer.get_primitive_standard_structure())) + + @property + def get_with_conventional_lattice(self: Material) -> Material: + """ + Convert a structure to its conventional cell. + """ + return Material(from_pymatgen(self.spacegroup_analyzer.get_conventional_standard_structure())) diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py b/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py index 62b8259a..f0cae122 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py @@ -2,12 +2,18 @@ from mat3ra.made.material import Material -from .builders import NanoparticleBuilder, ASEBasedNanoparticleBuilder -from .configuration import ASEBasedNanoparticleConfiguration, NanoparticleConfiguration +from .builders import SlabBasedNanoparticleBuilder, ASEBasedNanoparticleBuilder +from .configuration import ( + ASEBasedNanoparticleConfiguration, + SlabBasedNanoparticleConfiguration, + SphereSlabBasedNanoparticleConfiguration, +) def create_nanoparticle( - configuration: Union[NanoparticleConfiguration, ASEBasedNanoparticleConfiguration], - builder: Union[NanoparticleBuilder, ASEBasedNanoparticleBuilder] = NanoparticleBuilder(), + configuration: Union[ + SlabBasedNanoparticleBuilder, SphereSlabBasedNanoparticleConfiguration, ASEBasedNanoparticleConfiguration + ], + builder: Union[SlabBasedNanoparticleBuilder, ASEBasedNanoparticleBuilder] = SlabBasedNanoparticleBuilder(), ) -> Material: return builder.get_material(configuration) diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py index 24a06e98..b79dc331 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py @@ -66,9 +66,9 @@ class SphereSlabBasedNanoparticleBuilder(SlabBasedNanoparticleBuilder): Builder for creating spherical nanoparticles by cutting from bulk materials supercells. """ - _ConfigurationType: type(SphereSlabBasedNanoparticleConfiguration) = ( # type: ignore + _ConfigurationType: type( SphereSlabBasedNanoparticleConfiguration - ) + ) = SphereSlabBasedNanoparticleConfiguration # type: ignore def _build_coordinate_condition(self, config: _ConfigurationType, center_coordinate: List[float]) -> Callable: return SphereCoordinateCondition(center_position=center_coordinate, radius=config.radius).condition diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py b/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py index f3c5c19e..d08d7d4b 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py @@ -1,14 +1,10 @@ -from typing import Dict, Optional, Tuple +from typing import Dict, Optional, Tuple, Callable import numpy as np from mat3ra.made.material import Material -from mat3ra.made.tools.convert import decorator_convert_material_args_kwargs_to_structure -from pymatgen.symmetry.analyzer import SpacegroupAnalyzer -from ...convert import from_pymatgen -from ...third_party import PymatgenStructure -from .enums import NanoparticleShapes +from .enums import ASENanoparticleShapesEnum from ...build import BaseConfiguration @@ -35,10 +31,6 @@ def lattice_constant(self) -> float: lattice_constant = lattice_constants[0] return lattice_constant - @property - def element(self) -> str: - return self.material.basis.elements.get_element_value_by_index(0) - @property def _json(self): return { @@ -46,30 +38,11 @@ def _json(self): "vacuum_padding": self.vacuum_padding, } - # TODO: move to a separate module - @staticmethod - @decorator_convert_material_args_kwargs_to_structure - def convert_to_primitive(structure: PymatgenStructure) -> Material: - """ - Convert a structure to its primitive cell. - """ - analyzer = SpacegroupAnalyzer(structure) - return Material(from_pymatgen(analyzer.get_primitive_standard_structure())) - - @staticmethod - @decorator_convert_material_args_kwargs_to_structure - def convert_to_conventional(structure: PymatgenStructure) -> Material: - """ - Convert a structure to its conventional cell. - """ - analyzer = SpacegroupAnalyzer(structure) - return Material(from_pymatgen(analyzer.get_conventional_standard_structure())) - - -class NanoparticleConfiguration(BaseNanoparticleConfiguration): - shape: NanoparticleShapes = NanoparticleShapes.ICOSAHEDRON # Shape of the nanoparticle + +class SlabBasedNanoparticleConfiguration(BaseNanoparticleConfiguration): + condition_builder: Callable # Function of the center coordinate to build the condition for filtering the slab that returns function of the coordinates + supercell_size: int = 1 # Size of the supercell in the xy-plane orientation_z: Tuple[int, int, int] = (0, 0, 1) # Orientation of the crystallographic axis in the z-direction - radius: float = 5.0 # Radius of the nanoparticle (largest feature size for a shape), in Angstroms @property def _json(self): @@ -77,6 +50,16 @@ def _json(self): **super()._json, "shape": self.shape.value, "orientation_z": self.orientation_z, + } + + +class SphereSlabBasedNanoparticleConfiguration(SlabBasedNanoparticleConfiguration): + radius: float = 5.0 # Radius of the nanoparticle, in Angstroms + + @property + def _json(self): + return { + **super()._json, "radius": self.radius, } @@ -92,9 +75,13 @@ class ASEBasedNanoparticleConfiguration(BaseNanoparticleConfiguration): vacuum_padding (float): Vacuum padding around the nanoparticle. """ - shape: NanoparticleShapes + shape: ASENanoparticleShapesEnum parameters: Optional[Dict] = None # Shape-specific parameters (e.g., layers, size) + @property + def element(self) -> str: + return self.material.basis.elements.get_element_value_by_index(0) + @property def _json(self): return { diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/enums.py b/src/py/mat3ra/made/tools/build/nanoparticle/enums.py index a53b1206..e78c9102 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/enums.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/enums.py @@ -1,12 +1,25 @@ from enum import Enum +from typing import Callable +from ...third_party import ASEAtoms +# TODO: import from 3rd party +from ase.cluster import ( + SimpleCubic, + BodyCenteredCubic, + FaceCenteredCubic, + Icosahedron, + Octahedron, + Decahedron, + HexagonalClosedPacked, +) +from ase.cluster.wulff import wulff_construction -class NanoparticleShapes(str, Enum): + +class ASENanoparticleShapesEnum(str, Enum): """ Enum for supported nanoparticle shapes. """ - SPHERE = "sphere" ICOSAHEDRON = "icosahedron" OCTAHEDRON = "octahedron" DECAHEDRON = "decahedron" @@ -14,4 +27,17 @@ class NanoparticleShapes(str, Enum): FACE_CENTERED_CUBIC = "face_centered_cubic" BODY_CENTERED_CUBIC = "body_centered_cubic" HEXAGONAL_CLOSED_PACKED = "hexagonal_closed_packed" - WULFF = "wulff" + WULFF_CONSTRUCTION = "wulff_construction" + + @staticmethod + def get_ase_constructor(shape: str) -> Callable[..., ASEAtoms]: + return { + "icosahedron": Icosahedron, + "octahedron": Octahedron, + "decahedron": Decahedron, + "simple_cubic": SimpleCubic, + "face_centered_cubic": FaceCenteredCubic, + "body_centered_cubic": BodyCenteredCubic, + "hexagonal_closed_packed": HexagonalClosedPacked, + "wulff_construction": wulff_construction, + }[shape] From a3e0bddb7987d420c0f677cd0c1ae3d1daae7b5b Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:13:29 -0800 Subject: [PATCH 12/14] chore: moe to 3rd party --- .../made/tools/build/nanoparticle/builders.py | 4 +- .../made/tools/build/nanoparticle/enums.py | 38 +++++++++---------- src/py/mat3ra/made/tools/third_party.py | 16 ++++++++ 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py index b79dc331..3f2b7f6b 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py @@ -66,9 +66,7 @@ class SphereSlabBasedNanoparticleBuilder(SlabBasedNanoparticleBuilder): Builder for creating spherical nanoparticles by cutting from bulk materials supercells. """ - _ConfigurationType: type( - SphereSlabBasedNanoparticleConfiguration - ) = SphereSlabBasedNanoparticleConfiguration # type: ignore + _ConfigurationType: type(SphereSlabBasedNanoparticleConfiguration) = SphereSlabBasedNanoparticleConfiguration def _build_coordinate_condition(self, config: _ConfigurationType, center_coordinate: List[float]) -> Callable: return SphereCoordinateCondition(center_position=center_coordinate, radius=config.radius).condition diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/enums.py b/src/py/mat3ra/made/tools/build/nanoparticle/enums.py index e78c9102..6e4524df 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/enums.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/enums.py @@ -1,18 +1,16 @@ from enum import Enum from typing import Callable -from ...third_party import ASEAtoms - -# TODO: import from 3rd party -from ase.cluster import ( - SimpleCubic, - BodyCenteredCubic, - FaceCenteredCubic, - Icosahedron, - Octahedron, - Decahedron, - HexagonalClosedPacked, +from ...third_party import ( + ASEAtoms, + ASESimpleCubic, + ASEBodyCenteredCubic, + ASEFaceCenteredCubic, + ASEIcosahedron, + ASEOctahedron, + ASEDecahedron, + ASEHexagonalClosedPacked, + ASEWulffConstruction, ) -from ase.cluster.wulff import wulff_construction class ASENanoparticleShapesEnum(str, Enum): @@ -32,12 +30,12 @@ class ASENanoparticleShapesEnum(str, Enum): @staticmethod def get_ase_constructor(shape: str) -> Callable[..., ASEAtoms]: return { - "icosahedron": Icosahedron, - "octahedron": Octahedron, - "decahedron": Decahedron, - "simple_cubic": SimpleCubic, - "face_centered_cubic": FaceCenteredCubic, - "body_centered_cubic": BodyCenteredCubic, - "hexagonal_closed_packed": HexagonalClosedPacked, - "wulff_construction": wulff_construction, + "icosahedron": ASEIcosahedron, + "octahedron": ASEOctahedron, + "decahedron": ASEDecahedron, + "simple_cubic": ASESimpleCubic, + "face_centered_cubic": ASEFaceCenteredCubic, + "body_centered_cubic": ASEBodyCenteredCubic, + "hexagonal_closed_packed": ASEHexagonalClosedPacked, + "ase_wulff_construction": ASEWulffConstruction, }[shape] diff --git a/src/py/mat3ra/made/tools/third_party.py b/src/py/mat3ra/made/tools/third_party.py index 9858d917..e862e2e7 100644 --- a/src/py/mat3ra/made/tools/third_party.py +++ b/src/py/mat3ra/made/tools/third_party.py @@ -4,6 +4,14 @@ from ase.calculators.calculator import Calculator as ASECalculator from ase.calculators.calculator import all_changes as ase_all_changes from ase.calculators.emt import EMT as ASECalculatorEMT +from ase.cluster import BodyCenteredCubic as ASEBodyCenteredCubic +from ase.cluster import Decahedron as ASEDecahedron +from ase.cluster import FaceCenteredCubic as ASEFaceCenteredCubic +from ase.cluster import HexagonalClosedPacked as ASEHexagonalClosedPacked +from ase.cluster import Icosahedron as ASEIcosahedron +from ase.cluster import Octahedron as ASEOctahedron +from ase.cluster import SimpleCubic as ASESimpleCubic +from ase.cluster.wulff import wulff_construction as ASEWulffConstruction from ase.constraints import FixAtoms as ASEFixAtoms from ase.constraints import FixedPlane as ASEFixedPlane from pymatgen.analysis.defects.core import Interstitial as PymatgenInterstitial @@ -48,4 +56,12 @@ "PymatgenAseAtomsAdaptor", "PymatgenPoscar", "PymatgenVoronoiNN", + "ASESimpleCubic", + "ASEBodyCenteredCubic", + "ASEFaceCenteredCubic", + "ASEIcosahedron", + "ASEOctahedron", + "ASEDecahedron", + "ASEHexagonalClosedPacked", + "ASEWulffConstruction", ] From a6b7d5da0cb14abf8ae6826bcde2054a2675c0c6 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:14:59 -0800 Subject: [PATCH 13/14] chore: imports --- .../made/tools/build/nanoparticle/builders.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py index 3f2b7f6b..566a0cbf 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py @@ -1,18 +1,17 @@ from typing import List, Callable, Dict from mat3ra.made.material import Material -from mat3ra.made.tools.analyze.other import get_chemical_formula -from mat3ra.made.tools.build import BaseBuilder -from mat3ra.made.tools.build.mixins import ConvertGeneratedItemsASEAtomsMixin -from mat3ra.made.tools.build.slab import SlabConfiguration -from mat3ra.made.tools.modify import filter_by_condition_on_coordinates -from mat3ra.made.tools.utils.coordinate import SphereCoordinateCondition - -from .configuration import ASEBasedNanoparticleConfiguration, SphereSlabBasedNanoparticleConfiguration -from .enums import ASENanoparticleShapesEnum -from ..slab import create_slab +from ...analyze.other import get_chemical_formula +from ...build import BaseBuilder +from ...build.mixins import ConvertGeneratedItemsASEAtomsMixin +from ...build.slab import SlabConfiguration +from ...modify import filter_by_condition_on_coordinates +from ...utils.coordinate import SphereCoordinateCondition from ...analyze.other import get_closest_site_id_from_coordinate from ...third_party import ASEAtoms +from ..slab import create_slab +from .configuration import ASEBasedNanoparticleConfiguration, SphereSlabBasedNanoparticleConfiguration +from .enums import ASENanoparticleShapesEnum class SlabBasedNanoparticleBuilder(BaseBuilder): From 82951d1264becf60343b0c16bcc5c89ac748bed9 Mon Sep 17 00:00:00 2001 From: VsevolodX <79542055+VsevolodX@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:35:59 -0800 Subject: [PATCH 14/14] chore: run lint fix and types --- src/py/mat3ra/made/tools/analyze/lattice.py | 6 ++---- src/py/mat3ra/made/tools/build/nanoparticle/builders.py | 8 +++++--- .../mat3ra/made/tools/build/nanoparticle/configuration.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/py/mat3ra/made/tools/analyze/lattice.py b/src/py/mat3ra/made/tools/analyze/lattice.py index 45c26580..976b2dde 100644 --- a/src/py/mat3ra/made/tools/analyze/lattice.py +++ b/src/py/mat3ra/made/tools/analyze/lattice.py @@ -1,13 +1,11 @@ from mat3ra.made.material import Material from mat3ra.made.tools.analyze import BaseMaterialAnalyzer -from mat3ra.made.tools.convert import from_pymatgen -from ..third_party import PymatgenSpacegroupAnalyzer +from mat3ra.made.tools.convert import from_pymatgen, to_pymatgen -from mat3ra.made.tools.convert import to_pymatgen +from ..third_party import PymatgenSpacegroupAnalyzer class LatticeMaterialAnalyzer(BaseMaterialAnalyzer): - def __init__(self, material: Material): super().__init__(material) self.spacegroup_analyzer = PymatgenSpacegroupAnalyzer(to_pymatgen(self.material)) diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py index 566a0cbf..e8d60681 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/builders.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/builders.py @@ -1,4 +1,4 @@ -from typing import List, Callable, Dict +from typing import List, Callable, Dict, Type from mat3ra.made.material import Material from ...analyze.other import get_chemical_formula @@ -65,9 +65,11 @@ class SphereSlabBasedNanoparticleBuilder(SlabBasedNanoparticleBuilder): Builder for creating spherical nanoparticles by cutting from bulk materials supercells. """ - _ConfigurationType: type(SphereSlabBasedNanoparticleConfiguration) = SphereSlabBasedNanoparticleConfiguration + _ConfigurationType: Type[ASEBasedNanoparticleConfiguration] = SphereSlabBasedNanoparticleConfiguration - def _build_coordinate_condition(self, config: _ConfigurationType, center_coordinate: List[float]) -> Callable: + def _build_coordinate_condition( + self, config: SphereSlabBasedNanoparticleConfiguration, center_coordinate: List[float] + ) -> Callable: return SphereCoordinateCondition(center_position=center_coordinate, radius=config.radius).condition diff --git a/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py b/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py index d08d7d4b..762ede8c 100644 --- a/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py +++ b/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py @@ -40,7 +40,7 @@ def _json(self): class SlabBasedNanoparticleConfiguration(BaseNanoparticleConfiguration): - condition_builder: Callable # Function of the center coordinate to build the condition for filtering the slab that returns function of the coordinates + condition_builder: Callable # Function of the center coordinate to build the condition for filtering the slab supercell_size: int = 1 # Size of the supercell in the xy-plane orientation_z: Tuple[int, int, int] = (0, 0, 1) # Orientation of the crystallographic axis in the z-direction