@mat3ra/made 2024.6.11-0 → 2024.6.12-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mat3ra/made",
3
- "version": "2024.6.11-0",
3
+ "version": "2024.6.12-0",
4
4
  "description": "MAterials DEsign library",
5
5
  "scripts": {
6
6
  "lint": "eslint --cache src/js tests/js && prettier --write src/js tests/js",
package/pyproject.toml CHANGED
@@ -27,6 +27,7 @@ dependencies = [
27
27
  tools = [
28
28
  "pymatgen",
29
29
  "ase",
30
+ "pymatgen-analysis-defects"
30
31
  ]
31
32
  dev = [
32
33
  "pre-commit",
@@ -3,6 +3,7 @@ from typing import Any, Dict, List, Union
3
3
  from mat3ra.code.constants import AtomicCoordinateUnits, Units
4
4
  from mat3ra.code.entity import HasDescriptionHasMetadataNamedDefaultableInMemoryEntity
5
5
  from mat3ra.esse.models.material import MaterialSchema
6
+ from mat3ra.made.utils import map_array_with_id_value_to_array
6
7
 
7
8
  defaultMaterialConfig = {
8
9
  "name": "Silicon FCC",
@@ -57,3 +58,7 @@ class Material(HasDescriptionHasMetadataNamedDefaultableInMemoryEntity):
57
58
 
58
59
  def to_json(self, exclude: List[str] = []) -> MaterialSchemaJSON:
59
60
  return {**super().to_json()}
61
+
62
+ @property
63
+ def coordinates_array(self) -> List[List[float]]:
64
+ return map_array_with_id_value_to_array(self.basis["coordinates"])
@@ -1,6 +1,9 @@
1
+ from typing import List
2
+
1
3
  import numpy as np
2
4
  from ase import Atoms
3
5
 
6
+ from ..material import Material
4
7
  from .convert import decorator_convert_material_args_kwargs_to_atoms
5
8
 
6
9
 
@@ -69,3 +72,20 @@ def get_chemical_formula(atoms: Atoms):
69
72
  str: The formula of the atoms.
70
73
  """
71
74
  return atoms.get_chemical_formula()
75
+
76
+
77
+ def get_closest_site_id_from_position(material: Material, position: List[float]) -> int:
78
+ """
79
+ Get the site ID of the closest site to a given position in the crystal.
80
+
81
+ Args:
82
+ material (Material): The material object to find the closest site in.
83
+ position (List[float]): The position to find the closest site to.
84
+
85
+ Returns:
86
+ int: The site ID of the closest site.
87
+ """
88
+ coordinates = np.array(material.coordinates_array)
89
+ position = np.array(position) # type: ignore
90
+ distances = np.linalg.norm(coordinates - position, axis=1)
91
+ return int(np.argmin(distances))
@@ -0,0 +1,36 @@
1
+ from typing import Optional
2
+
3
+ from mat3ra.utils.factory import BaseFactory
4
+ from mat3ra.made.material import Material
5
+
6
+ from .builders import PointDefectBuilderParameters
7
+ from .configuration import PointDefectConfiguration
8
+ from .enums import PointDefectTypeEnum
9
+
10
+
11
+ class DefectBuilderFactory(BaseFactory):
12
+ __class_registry__ = {
13
+ PointDefectTypeEnum.VACANCY: "mat3ra.made.tools.build.defect.builders.VacancyPointDefectBuilder",
14
+ PointDefectTypeEnum.SUBSTITUTION: "mat3ra.made.tools.build.defect.builders.SubstitutionPointDefectBuilder",
15
+ PointDefectTypeEnum.INTERSTITIAL: "mat3ra.made.tools.build.defect.builders.InterstitialPointDefectBuilder",
16
+ }
17
+
18
+
19
+ def create_defect(
20
+ configuration: PointDefectConfiguration,
21
+ builder_parameters: Optional[PointDefectBuilderParameters] = None,
22
+ ) -> Material:
23
+ """
24
+ Return a material with a selected defect added.
25
+
26
+ Args:
27
+ configuration: The configuration of the defect to be added.
28
+ builder_parameters: The parameters to be used by the defect builder.
29
+
30
+ Returns:
31
+ The material with the defect added.
32
+ """
33
+ BuilderClass = DefectBuilderFactory.get_class_by_name(configuration.defect_type)
34
+ builder = BuilderClass(builder_parameters)
35
+
36
+ return builder.get_material(configuration) if builder else configuration.crystal
@@ -0,0 +1,69 @@
1
+ from typing import List, Callable
2
+
3
+ from mat3ra.made.material import Material
4
+ from pydantic import BaseModel
5
+ from pymatgen.analysis.defects.core import (
6
+ Substitution as PymatgenSubstitution,
7
+ Vacancy as PymatgenVacancy,
8
+ Interstitial as PymatgenInterstitial,
9
+ )
10
+ from pymatgen.core import PeriodicSite as PymatgenPeriodicSite
11
+
12
+ from ...build import BaseBuilder
13
+ from ...convert import PymatgenStructure, to_pymatgen
14
+ from ..mixins import ConvertGeneratedItemsPymatgenStructureMixin
15
+ from .configuration import PointDefectConfiguration
16
+ from mat3ra.made.utils import get_array_with_id_value_element_value_by_index
17
+
18
+
19
+ class PointDefectBuilderParameters(BaseModel):
20
+ center_defect: bool = False
21
+
22
+
23
+ class PointDefectBuilder(ConvertGeneratedItemsPymatgenStructureMixin, BaseBuilder):
24
+ """
25
+ Builder class for generating point defects.
26
+ """
27
+
28
+ _BuildParametersType = PointDefectBuilderParameters
29
+ _DefaultBuildParameters = PointDefectBuilderParameters()
30
+ _GeneratedItemType: PymatgenStructure = PymatgenStructure
31
+ _ConfigurationType = PointDefectConfiguration
32
+ _generator: Callable
33
+
34
+ def _get_species(self, configuration: BaseBuilder._ConfigurationType):
35
+ crystal_elements = configuration.crystal.basis["elements"]
36
+ placeholder_specie = get_array_with_id_value_element_value_by_index(crystal_elements, 0)
37
+ return configuration.chemical_element or placeholder_specie
38
+
39
+ def _generate(self, configuration: BaseBuilder._ConfigurationType) -> List[_GeneratedItemType]:
40
+ pymatgen_structure = to_pymatgen(configuration.crystal)
41
+ pymatgen_periodic_site = PymatgenPeriodicSite(
42
+ species=self._get_species(configuration),
43
+ coords=configuration.position,
44
+ lattice=pymatgen_structure.lattice,
45
+ )
46
+ defect = self._generator(pymatgen_structure, pymatgen_periodic_site)
47
+ defect_structure = defect.defect_structure.copy()
48
+ defect_structure.remove_oxidation_states()
49
+ return [defect_structure]
50
+
51
+ def _update_material_name(self, material: Material, configuration: BaseBuilder._ConfigurationType) -> Material:
52
+ updated_material = super()._update_material_name(material, configuration)
53
+ capitalized_defect_type = configuration.defect_type.name.capitalize()
54
+ chemical_element = configuration.chemical_element if configuration.chemical_element else ""
55
+ new_name = f"{updated_material.name}, {capitalized_defect_type} {chemical_element} Defect"
56
+ updated_material.name = new_name
57
+ return updated_material
58
+
59
+
60
+ class VacancyPointDefectBuilder(PointDefectBuilder):
61
+ _generator: PymatgenVacancy = PymatgenVacancy
62
+
63
+
64
+ class SubstitutionPointDefectBuilder(PointDefectBuilder):
65
+ _generator: PymatgenSubstitution = PymatgenSubstitution
66
+
67
+
68
+ class InterstitialPointDefectBuilder(PointDefectBuilder):
69
+ _generator: PymatgenInterstitial = PymatgenInterstitial
@@ -0,0 +1,44 @@
1
+ from typing import Optional, List, Any
2
+ from pydantic import BaseModel
3
+
4
+ from mat3ra.code.entity import InMemoryEntity
5
+ from mat3ra.made.material import Material
6
+
7
+ from ...analyze import get_closest_site_id_from_position
8
+ from .enums import PointDefectTypeEnum
9
+
10
+
11
+ class BaseDefectConfiguration(BaseModel):
12
+ # TODO: fix arbitrary_types_allowed error and set Material class type
13
+ crystal: Any = None
14
+
15
+
16
+ class PointDefectConfiguration(BaseDefectConfiguration, InMemoryEntity):
17
+ defect_type: PointDefectTypeEnum
18
+ position: Optional[List[float]] = [0, 0, 0] # fractional coordinates
19
+ site_id: Optional[int] = None
20
+ chemical_element: Optional[str] = None
21
+
22
+ def __init__(self, position=position, site_id=None, **data):
23
+ super().__init__(**data)
24
+ if site_id is not None:
25
+ self.position = self.crystal.coordinates_array[site_id]
26
+ else:
27
+ self.site_id = get_closest_site_id_from_position(self.crystal, position)
28
+
29
+ @classmethod
30
+ def from_site_id(cls, site_id: int, crystal: Material, **data):
31
+ if crystal:
32
+ position = crystal.coordinates_array[site_id]
33
+ else:
34
+ RuntimeError("Crystal is not defined")
35
+ return cls(crystal=crystal, position=position, site_id=site_id, **data)
36
+
37
+ @property
38
+ def _json(self):
39
+ return {
40
+ "type": "PointDefectConfiguration",
41
+ "defect_type": self.defect_type.name,
42
+ "position": self.position,
43
+ "chemical_element": self.chemical_element,
44
+ }
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+
4
+ class PointDefectTypeEnum(str, Enum):
5
+ VACANCY = "vacancy"
6
+ SUBSTITUTION = "substitution"
7
+ INTERSTITIAL = "interstitial"
@@ -54,7 +54,7 @@ class SimpleInterfaceBuilder(ConvertGeneratedItemsASEAtomsMixin, InterfaceBuilde
54
54
  Creates matching interface between substrate and film by straining the film to match the substrate.
55
55
  """
56
56
 
57
- _BuildParametersType = Optional[SimpleInterfaceBuilderParameters]
57
+ _BuildParametersType = SimpleInterfaceBuilderParameters
58
58
  _DefaultBuildParameters = SimpleInterfaceBuilderParameters(scale_film=True)
59
59
  _GeneratedItemType: type(ASEAtoms) = ASEAtoms # type: ignore
60
60
 
@@ -116,9 +116,9 @@ class StrainMatchingInterfaceBuilder(InterfaceBuilder):
116
116
  updated_material = super()._update_material_name(material, configuration)
117
117
  if StrainModes.mean_abs_strain in material.metadata:
118
118
  strain = material.metadata[StrainModes.mean_abs_strain]
119
- new_name = f"{updated_material.name}, Strain {strain*100:.3f}%"
119
+ new_name = f"{updated_material.name}, Strain {strain*100:.3f}pct"
120
120
  updated_material.name = new_name
121
- return material
121
+ return updated_material
122
122
 
123
123
 
124
124
  class ZSLStrainMatchingParameters(BaseModel):
@@ -15,6 +15,10 @@ def map_array_with_id_value_to_array(array: List[Dict[str, Any]]) -> List[Any]:
15
15
  return [item["value"] for item in array]
16
16
 
17
17
 
18
+ def get_array_with_id_value_element_value_by_index(array: List[Dict[str, Any]], index: int = 0) -> List[Any]:
19
+ return map_array_with_id_value_to_array(array)[index]
20
+
21
+
18
22
  def filter_array_with_id_value_by_values(
19
23
  array: List[Dict[str, Any]], values: Union[List[Any], Any]
20
24
  ) -> List[Dict[str, Any]]:
@@ -56,7 +56,7 @@ INTERFACE_PROPERTIES_JSON = {
56
56
 
57
57
  # Add properties to interface structure
58
58
  INTERFACE_STRUCTURE.interface_properties = INTERFACE_PROPERTIES_MOCK
59
- INTERFACE_NAME = "Cu4(001)-Si8(001), Interface, Strain 0.062%"
59
+ INTERFACE_NAME = "Cu4(001)-Si8(001), Interface, Strain 0.062pct"
60
60
 
61
61
  # TODO: Use fixtures package when available
62
62
  SI_CONVENTIONAL_CELL = {
@@ -0,0 +1,51 @@
1
+ from mat3ra.made.material import Material
2
+ from mat3ra.made.tools.build.defect import PointDefectBuilderParameters, PointDefectConfiguration, create_defect
3
+
4
+ clean_material = Material.create(Material.default_config)
5
+
6
+
7
+ def test_create_vacancy():
8
+ # vacancy in place of 0 element
9
+ configuration = PointDefectConfiguration(crystal=clean_material, defect_type="vacancy", site_id=0)
10
+ defect = create_defect(configuration)
11
+
12
+ assert len(defect.basis["elements"]) == 1
13
+
14
+
15
+ def test_create_substitution():
16
+ # Substitution of Ge in place of Si at default site_id=0
17
+ configuration = PointDefectConfiguration(crystal=clean_material, defect_type="substitution", chemical_element="Ge")
18
+ defect = create_defect(configuration)
19
+
20
+ assert defect.basis["elements"] == [{"id": 0, "value": "Ge"}, {"id": 1, "value": "Si"}]
21
+ assert defect.basis["coordinates"][0] == {"id": 0, "value": [0.0, 0.0, 0.0]}
22
+
23
+
24
+ def test_create_interstitial():
25
+ # Interstitial Ge at 0.5, 0.5, 0.5 position
26
+ configuration = PointDefectConfiguration(
27
+ crystal=clean_material, defect_type="interstitial", chemical_element="Ge", position=[0.5, 0.5, 0.5]
28
+ )
29
+ defect = create_defect(configuration)
30
+
31
+ assert defect.basis["elements"] == [
32
+ {"id": 0, "value": "Ge"},
33
+ {"id": 1, "value": "Si"},
34
+ {"id": 2, "value": "Si"},
35
+ ]
36
+
37
+
38
+ def test_create_defect_from_site_id():
39
+ # Substitution of Ge in place of Si at site_id=1
40
+ defect_configuration = PointDefectConfiguration.from_site_id(
41
+ crystal=clean_material, defect_type="substitution", chemical_element="Ge", site_id=1
42
+ )
43
+ defect_builder_parameters = PointDefectBuilderParameters(center_defect=False)
44
+ material_with_defect = create_defect(
45
+ builder_parameters=defect_builder_parameters, configuration=defect_configuration
46
+ )
47
+
48
+ assert material_with_defect.basis["elements"] == [
49
+ {"id": 0, "value": "Si"},
50
+ {"id": 1, "value": "Ge"},
51
+ ]