@mat3ra/made 2025.7.15-0 → 2025.8.1-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.
Files changed (198) hide show
  1. package/README.md +7 -0
  2. package/dist/js/material.d.ts +7 -2
  3. package/package.json +1 -1
  4. package/pyproject.toml +4 -2
  5. package/src/py/mat3ra/made/basis/__init__.py +87 -8
  6. package/src/py/mat3ra/made/lattice.py +19 -7
  7. package/src/py/mat3ra/made/material.py +20 -3
  8. package/src/py/mat3ra/made/metadata.py +45 -0
  9. package/src/py/mat3ra/made/tools/analyze/__init__.py +14 -4
  10. package/src/py/mat3ra/made/tools/analyze/adatom.py +75 -0
  11. package/src/py/mat3ra/made/tools/analyze/crystal_site.py +108 -0
  12. package/src/py/mat3ra/made/tools/analyze/interface/__init__.py +24 -0
  13. package/src/py/mat3ra/made/tools/analyze/interface/commensurate.py +155 -0
  14. package/src/py/mat3ra/made/tools/analyze/interface/enums.py +60 -0
  15. package/src/py/mat3ra/made/tools/analyze/interface/grain_boundary.py +79 -0
  16. package/src/py/mat3ra/made/tools/analyze/interface/simple.py +165 -0
  17. package/src/py/mat3ra/made/tools/analyze/interface/twisted_nanoribbons.py +89 -0
  18. package/src/py/mat3ra/made/tools/analyze/interface/utils/__init__.py +5 -0
  19. package/src/py/mat3ra/made/tools/analyze/interface/utils/holders.py +11 -0
  20. package/src/py/mat3ra/made/tools/analyze/interface/utils/vector.py +56 -0
  21. package/src/py/mat3ra/made/tools/analyze/interface/zsl.py +190 -0
  22. package/src/py/mat3ra/made/tools/analyze/lattice.py +32 -8
  23. package/src/py/mat3ra/made/tools/analyze/lattice_lines.py +29 -0
  24. package/src/py/mat3ra/made/tools/analyze/lattice_planes.py +192 -0
  25. package/src/py/mat3ra/made/tools/analyze/other.py +26 -42
  26. package/src/py/mat3ra/made/tools/analyze/rdf.py +1 -1
  27. package/src/py/mat3ra/made/tools/analyze/slab.py +89 -0
  28. package/src/py/mat3ra/made/tools/analyze/terrace.py +105 -0
  29. package/src/py/mat3ra/made/tools/analyze/utils.py +51 -0
  30. package/src/py/mat3ra/made/tools/build/__init__.py +85 -18
  31. package/src/py/mat3ra/made/tools/build/defect/__init__.py +0 -111
  32. package/src/py/mat3ra/made/tools/build/defect/adatom/builders.py +11 -0
  33. package/src/py/mat3ra/made/tools/build/defect/adatom/configuration.py +18 -0
  34. package/src/py/mat3ra/made/tools/build/defect/adatom/helpers.py +128 -0
  35. package/src/py/mat3ra/made/tools/build/defect/enums.py +21 -7
  36. package/src/py/mat3ra/made/tools/build/defect/factories.py +68 -12
  37. package/src/py/mat3ra/made/tools/build/defect/island/builders.py +15 -0
  38. package/src/py/mat3ra/made/tools/build/defect/island/configuration.py +16 -0
  39. package/src/py/mat3ra/made/tools/build/defect/island/helpers.py +113 -0
  40. package/src/py/mat3ra/made/tools/build/defect/pair_defect/__init__.py +0 -0
  41. package/src/py/mat3ra/made/tools/build/defect/pair_defect/analyzer.py +15 -0
  42. package/src/py/mat3ra/made/tools/build/defect/pair_defect/builders.py +25 -0
  43. package/src/py/mat3ra/made/tools/build/defect/pair_defect/configuration.py +41 -0
  44. package/src/py/mat3ra/made/tools/build/defect/pair_defect/helpers.py +65 -0
  45. package/src/py/mat3ra/made/tools/build/defect/point/builders.py +109 -0
  46. package/src/py/mat3ra/made/tools/build/defect/point/configuration.py +82 -0
  47. package/src/py/mat3ra/made/tools/build/defect/point/helpers.py +211 -0
  48. package/src/py/mat3ra/made/tools/build/defect/slab/__init__.py +0 -0
  49. package/src/py/mat3ra/made/tools/build/defect/slab/builders.py +32 -0
  50. package/src/py/mat3ra/made/tools/build/defect/slab/configuration.py +33 -0
  51. package/src/py/mat3ra/made/tools/build/defect/slab/helpers.py +78 -0
  52. package/src/py/mat3ra/made/tools/build/defect/terrace/builders.py +25 -0
  53. package/src/py/mat3ra/made/tools/build/defect/terrace/configuration.py +8 -0
  54. package/src/py/mat3ra/made/tools/build/defect/terrace/helpers.py +76 -0
  55. package/src/py/mat3ra/made/tools/build/defect/terrace/parameters.py +10 -0
  56. package/src/py/mat3ra/made/tools/build/grain_boundary/__init__.py +10 -23
  57. package/src/py/mat3ra/made/tools/build/grain_boundary/builders.py +36 -110
  58. package/src/py/mat3ra/made/tools/build/grain_boundary/configuration.py +76 -48
  59. package/src/py/mat3ra/made/tools/build/grain_boundary/helpers.py +153 -0
  60. package/src/py/mat3ra/made/tools/build/interface/__init__.py +12 -78
  61. package/src/py/mat3ra/made/tools/build/interface/builders.py +135 -390
  62. package/src/py/mat3ra/made/tools/build/interface/configuration.py +45 -94
  63. package/src/py/mat3ra/made/tools/build/interface/enums.py +0 -59
  64. package/src/py/mat3ra/made/tools/build/interface/helpers.py +445 -0
  65. package/src/py/mat3ra/made/tools/build/interface/utils.py +4 -15
  66. package/src/py/mat3ra/made/tools/build/lattice_lines/__init__.py +52 -0
  67. package/src/py/mat3ra/made/tools/build/lattice_lines/builders.py +75 -0
  68. package/src/py/mat3ra/made/tools/build/lattice_lines/configuration.py +62 -0
  69. package/src/py/mat3ra/made/tools/build/merge/__init__.py +6 -0
  70. package/src/py/mat3ra/made/tools/build/merge/builders.py +93 -0
  71. package/src/py/mat3ra/made/tools/build/merge/configuration.py +21 -0
  72. package/src/py/mat3ra/made/tools/build/metadata.py +39 -0
  73. package/src/py/mat3ra/made/tools/build/monolayer/__init__.py +0 -0
  74. package/src/py/mat3ra/made/tools/build/monolayer/builders.py +75 -0
  75. package/src/py/mat3ra/made/tools/build/monolayer/configurations.py +21 -0
  76. package/src/py/mat3ra/made/tools/build/monolayer/helpers.py +40 -0
  77. package/src/py/mat3ra/made/tools/build/nanoparticle/__init__.py +0 -19
  78. package/src/py/mat3ra/made/tools/build/nanoparticle/analyzer.py +86 -0
  79. package/src/py/mat3ra/made/tools/build/nanoparticle/builders.py +27 -64
  80. package/src/py/mat3ra/made/tools/build/nanoparticle/configuration.py +18 -76
  81. package/src/py/mat3ra/made/tools/build/nanoparticle/enums.py +1 -1
  82. package/src/py/mat3ra/made/tools/build/nanoparticle/helpers.py +119 -0
  83. package/src/py/mat3ra/made/tools/build/nanoribbon/__init__.py +21 -7
  84. package/src/py/mat3ra/made/tools/build/nanoribbon/builders.py +48 -150
  85. package/src/py/mat3ra/made/tools/build/nanoribbon/configuration.py +24 -28
  86. package/src/py/mat3ra/made/tools/build/nanoribbon/helpers.py +73 -0
  87. package/src/py/mat3ra/made/tools/build/nanotape/__init__.py +10 -0
  88. package/src/py/mat3ra/made/tools/build/nanotape/builders.py +83 -0
  89. package/src/py/mat3ra/made/tools/build/nanotape/configuration.py +35 -0
  90. package/src/py/mat3ra/made/tools/build/nanotape/helpers.py +60 -0
  91. package/src/py/mat3ra/made/tools/build/passivation/__init__.py +0 -60
  92. package/src/py/mat3ra/made/tools/build/passivation/analyzer.py +150 -0
  93. package/src/py/mat3ra/made/tools/build/passivation/builders.py +14 -218
  94. package/src/py/mat3ra/made/tools/build/passivation/configuration.py +16 -15
  95. package/src/py/mat3ra/made/tools/build/passivation/helpers.py +151 -0
  96. package/src/py/mat3ra/made/tools/build/perturbation/__init__.py +0 -37
  97. package/src/py/mat3ra/made/tools/build/perturbation/builders.py +33 -58
  98. package/src/py/mat3ra/made/tools/build/perturbation/configuration.py +5 -19
  99. package/src/py/mat3ra/made/tools/build/perturbation/helpers.py +42 -0
  100. package/src/py/mat3ra/made/tools/build/perturbation/parameters.py +5 -0
  101. package/src/py/mat3ra/made/tools/build/slab/__init__.py +0 -47
  102. package/src/py/mat3ra/made/tools/build/slab/build_parameters.py +8 -0
  103. package/src/py/mat3ra/made/tools/build/slab/builders.py +117 -99
  104. package/src/py/mat3ra/made/tools/build/slab/configurations/__init__.py +15 -0
  105. package/src/py/mat3ra/made/tools/build/slab/configurations/base_configurations.py +33 -0
  106. package/src/py/mat3ra/made/tools/build/slab/configurations/slab_configuration.py +89 -0
  107. package/src/py/mat3ra/made/tools/build/slab/configurations/strained_configurations.py +12 -0
  108. package/src/py/mat3ra/made/tools/build/slab/entities.py +57 -0
  109. package/src/py/mat3ra/made/tools/build/slab/helpers.py +140 -0
  110. package/src/py/mat3ra/made/tools/build/slab/termination_utils.py +14 -0
  111. package/src/py/mat3ra/made/tools/build/slab/utils.py +49 -0
  112. package/src/py/mat3ra/made/tools/build/stack/builders.py +69 -0
  113. package/src/py/mat3ra/made/tools/build/stack/configuration.py +25 -0
  114. package/src/py/mat3ra/made/tools/build/supercell.py +26 -14
  115. package/src/py/mat3ra/made/tools/build/utils.py +21 -108
  116. package/src/py/mat3ra/made/tools/build/vacuum/builders.py +36 -0
  117. package/src/py/mat3ra/made/tools/build/vacuum/configuration.py +16 -0
  118. package/src/py/mat3ra/made/tools/build/void_region/builders.py +29 -0
  119. package/src/py/mat3ra/made/tools/build/void_region/configuration.py +19 -0
  120. package/src/py/mat3ra/made/tools/calculate/__init__.py +15 -11
  121. package/src/py/mat3ra/made/tools/calculate/calculators.py +3 -3
  122. package/src/py/mat3ra/made/tools/convert/__init__.py +8 -7
  123. package/src/py/mat3ra/made/tools/enums.py +1 -1
  124. package/src/py/mat3ra/made/tools/modify.py +30 -24
  125. package/src/py/mat3ra/made/tools/operations/core/binary.py +106 -0
  126. package/src/py/mat3ra/made/tools/operations/core/unary.py +101 -0
  127. package/src/py/mat3ra/made/tools/operations/core/utils.py +138 -0
  128. package/src/py/mat3ra/made/tools/operations/reusable/unary.py +28 -0
  129. package/src/py/mat3ra/made/tools/site.py +8 -3
  130. package/src/py/mat3ra/made/tools/utils/__init__.py +42 -17
  131. package/src/py/mat3ra/made/tools/utils/coordinate.py +2 -2
  132. package/src/py/mat3ra/made/tools/utils/functions.py +73 -23
  133. package/src/py/mat3ra/made/tools/utils/perturbation.py +3 -71
  134. package/src/py/mat3ra/made/utils.py +63 -48
  135. package/{src/py/mat3ra/made → tests/py/mat3ra}/debug_utils.py +5 -3
  136. package/tests/py/unit/fixtures/adatom.py +60 -0
  137. package/tests/py/unit/fixtures/bulk.py +152 -0
  138. package/tests/py/unit/fixtures/generated/fixtures.py +72 -74
  139. package/tests/py/unit/fixtures/grain_boundary.py +563 -0
  140. package/tests/py/unit/fixtures/interface/commensurate.py +498 -0
  141. package/tests/py/unit/fixtures/interface/gr_ni_111_top_hcp.py +102 -0
  142. package/tests/py/unit/fixtures/interface/simple.py +84 -0
  143. package/tests/py/unit/fixtures/interface/twisted_nanoribbons.py +131 -0
  144. package/tests/py/unit/fixtures/interface/zsl.py +540 -0
  145. package/tests/py/unit/fixtures/island.py +336 -0
  146. package/tests/py/unit/fixtures/merge.py +147 -0
  147. package/tests/py/unit/fixtures/monolayer.py +26 -0
  148. package/tests/py/unit/fixtures/nanoparticle.py +116 -0
  149. package/tests/py/unit/fixtures/nanoribbon/__init__.py +0 -0
  150. package/tests/py/unit/fixtures/nanoribbon/armchair.py +102 -0
  151. package/tests/py/unit/fixtures/{nanoribbon.py → nanoribbon/nanoribbon.py} +22 -103
  152. package/tests/py/unit/fixtures/nanoribbon/zigzag.py +54 -0
  153. package/tests/py/unit/fixtures/pair_defects.py +41 -0
  154. package/tests/py/unit/fixtures/passivated/nanoribbon.py +69 -0
  155. package/tests/py/unit/fixtures/passivated/slab.py +58 -0
  156. package/tests/py/unit/fixtures/point_defects.py +158 -0
  157. package/tests/py/unit/fixtures/slab.py +433 -127
  158. package/tests/py/unit/fixtures/{cell.py → strain.py} +4 -31
  159. package/tests/py/unit/fixtures/supercell.py +22 -0
  160. package/tests/py/unit/fixtures/terrace.py +70 -0
  161. package/tests/py/unit/test_analyze_lattice_planes.py +131 -0
  162. package/tests/py/unit/test_material.py +3 -3
  163. package/tests/py/unit/test_operations.py +80 -0
  164. package/tests/py/unit/test_tools_analyze.py +176 -17
  165. package/tests/py/unit/test_tools_analyze_interface.py +130 -0
  166. package/tests/py/unit/test_tools_analyze_interface_zsl.py +165 -0
  167. package/tests/py/unit/test_tools_build.py +135 -16
  168. package/tests/py/unit/test_tools_build_defect/__init__.py +0 -0
  169. package/tests/py/unit/test_tools_build_defect/test_adatom.py +72 -0
  170. package/tests/py/unit/test_tools_build_defect/test_island.py +43 -0
  171. package/tests/py/unit/test_tools_build_defect/test_pair_defect.py +80 -0
  172. package/tests/py/unit/test_tools_build_defect/test_point_defect.py +139 -0
  173. package/tests/py/unit/test_tools_build_defect/test_slab_stack.py +27 -0
  174. package/tests/py/unit/test_tools_build_defect/test_terrace.py +42 -0
  175. package/tests/py/unit/test_tools_build_grain_boundary.py +97 -75
  176. package/tests/py/unit/test_tools_build_interface.py +198 -68
  177. package/tests/py/unit/test_tools_build_interface_zsl.py +156 -0
  178. package/tests/py/unit/test_tools_build_metadata.py +80 -0
  179. package/tests/py/unit/test_tools_build_monolayer.py +34 -0
  180. package/tests/py/unit/test_tools_build_nanoparticle.py +39 -0
  181. package/tests/py/unit/test_tools_build_nanoribbon.py +31 -22
  182. package/tests/py/unit/test_tools_build_passivation.py +75 -32
  183. package/tests/py/unit/test_tools_build_perturbation.py +80 -28
  184. package/tests/py/unit/test_tools_build_slab.py +335 -25
  185. package/tests/py/unit/test_tools_build_supercell.py +20 -4
  186. package/tests/py/unit/test_tools_calculate.py +3 -2
  187. package/tests/py/unit/test_tools_convert.py +46 -9
  188. package/tests/py/unit/test_tools_modify.py +35 -27
  189. package/tests/py/unit/utils.py +39 -4
  190. package/src/py/mat3ra/made/tools/build/defect/builders.py +0 -767
  191. package/src/py/mat3ra/made/tools/build/defect/configuration.py +0 -342
  192. package/src/py/mat3ra/made/tools/build/interface/commensurate_lattice_pair.py +0 -26
  193. package/src/py/mat3ra/made/tools/build/interface/termination_pair.py +0 -63
  194. package/src/py/mat3ra/made/tools/build/nanoribbon/enums.py +0 -10
  195. package/src/py/mat3ra/made/tools/build/slab/configuration.py +0 -42
  196. package/src/py/mat3ra/made/tools/build/slab/termination.py +0 -24
  197. package/tests/py/unit/fixtures/interface.py +0 -121
  198. package/tests/py/unit/test_tools_build_defect.py +0 -224
package/README.md CHANGED
@@ -178,6 +178,13 @@ pip install ".[tests]"
178
178
  pytest tests/py
179
179
  ```
180
180
 
181
+ To visualize material from a debugger, run the following command in the "Evaluate expression" console:
182
+
183
+ ```python
184
+ from mat3ra.debug_utils import debug_visualize_material; debug_visualize_material(material)
185
+ ```
186
+
187
+
181
188
  #### 5.2.2. Important Notes
182
189
 
183
190
  Conventions:
@@ -1,8 +1,13 @@
1
- import { HasConsistencyChecksHasMetadataNamedDefaultableInMemoryEntity } from "@mat3ra/code/dist/js/entity";
2
1
  import type { ConsistencyCheck, MaterialSchema } from "@mat3ra/esse/dist/js/types";
3
2
  import { type MaterialMixinConstructor, defaultMaterialConfig } from "./materialMixin";
4
3
  export { defaultMaterialConfig };
5
- declare const BaseInMemoryEntity: typeof HasConsistencyChecksHasMetadataNamedDefaultableInMemoryEntity;
4
+ declare const BaseInMemoryEntity: typeof import("@mat3ra/code/dist/js/entity").InMemoryEntity & import("@mat3ra/code/dist/js/entity/mixins/DefaultableMixin").DefaultableInMemoryEntityConstructor & {
5
+ createDefault<T extends import("@mat3ra/code/dist/js/utils/types").Constructor<import("@mat3ra/code/dist/js/entity").InMemoryEntity> & {
6
+ defaultConfig?: object | null | undefined;
7
+ }>(this: T): InstanceType<T> & {
8
+ isDefault: boolean;
9
+ };
10
+ } & import("@mat3ra/code/dist/js/entity/mixins/NamedEntityMixin").NamedInMemoryEntityConstructor & import("@mat3ra/code/dist/js/entity/mixins/HasMetadataMixin").HasMetadataInMemoryEntityConstructor & import("@mat3ra/code/dist/js/entity/mixins/HasConsistencyChecksMixin").HasConsistencyChecksInMemoryEntityConstructor;
6
11
  type BaseMaterial = MaterialMixinConstructor & typeof BaseInMemoryEntity;
7
12
  type MaterialSchemaWithConsistencyChecksAsString = Omit<MaterialSchema, "consistencyChecks"> & {
8
13
  consistencyChecks?: ConsistencyCheck[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mat3ra/made",
3
- "version": "2025.7.15-0",
3
+ "version": "2025.8.1-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
@@ -19,8 +19,9 @@ dependencies = [
19
19
  # new verison of numpy==2.0.0 is not handled by pymatgen yet
20
20
  "numpy<=1.26.4",
21
21
  "mat3ra-utils",
22
- "mat3ra-esse==2025.5.16post0",
23
- "mat3ra-code==2025.4.27.post0",
22
+ "mat3ra-esse @ git+https://github.com/Exabyte-io/esse.git@090fe8b4271d449c8329f53b2a7da35ea9965062",
23
+ "mat3ra-code"
24
+
24
25
  ]
25
26
 
26
27
  [project.optional-dependencies]
@@ -47,6 +48,7 @@ tests = [
47
48
  "gradio",
48
49
  "pydantic",
49
50
  "mat3ra-made[tools]",
51
+ "mat3ra-standata"
50
52
  ]
51
53
  all = [
52
54
  "mat3ra-made[tests]",
@@ -1,12 +1,33 @@
1
1
  from typing import Any, Dict, List, Optional, Union
2
2
 
3
+ import numpy as np
3
4
  from mat3ra.code.array_with_ids import ArrayWithIds
4
5
  from mat3ra.code.entity import InMemoryEntityPydantic
6
+ from mat3ra.esse.models.core.abstract.matrix_3x3 import Matrix3x3Schema
5
7
  from mat3ra.esse.models.material import BasisSchema, BasisUnitsEnum
6
8
  from mat3ra.made.basis.coordinates import Coordinates
7
9
  from mat3ra.made.cell import Cell
8
- from mat3ra.made.utils import get_overlapping_coordinates
9
10
  from pydantic import Field
11
+ from scipy.spatial import cKDTree
12
+
13
+
14
+ def get_overlapping_coordinates(
15
+ coordinate: List[float],
16
+ coordinates: List[List[float]],
17
+ threshold: float = 0.01,
18
+ ) -> List[List[float]]:
19
+ """
20
+ Find coordinates that are within a certain threshold of a given coordinate.
21
+
22
+ Args:
23
+ coordinate (List[float]): The coordinate.
24
+ coordinates (List[List[float]]): The list of coordinates.
25
+ threshold (float): The threshold for the distance, in the units of the coordinates.
26
+
27
+ Returns:
28
+ List[List[float]]: The list of overlapping coordinates.
29
+ """
30
+ return [c for c in coordinates if np.linalg.norm(np.array(c) - np.array(coordinate)) < threshold]
10
31
 
11
32
 
12
33
  class Basis(BasisSchema, InMemoryEntityPydantic):
@@ -15,6 +36,9 @@ class Basis(BasisSchema, InMemoryEntityPydantic):
15
36
  cell: Cell = Field(Cell(), exclude=True)
16
37
  labels: ArrayWithIds = Field(ArrayWithIds.from_values([]))
17
38
  constraints: ArrayWithIds = Field(ArrayWithIds.from_values([]))
39
+ DEFAULT_COORDINATE_PROXIMITY_TOLERANCE: float = Field(
40
+ 0.1, exclude=True
41
+ ) # Angstroms, used for checking overlapping coordinates
18
42
 
19
43
  def __convert_kwargs__(self, **kwargs: Any) -> Dict[str, Any]:
20
44
  if isinstance(kwargs.get("elements"), list):
@@ -33,6 +57,17 @@ class Basis(BasisSchema, InMemoryEntityPydantic):
33
57
  kwargs = self.__convert_kwargs__(**kwargs)
34
58
  super().__init__(*args, **kwargs)
35
59
 
60
+ @property
61
+ def coordinates_as_kdtree(self):
62
+ return cKDTree(np.array(self.coordinates.values))
63
+
64
+ def get_coordinates_colliding_pairs(self, tolerance=DEFAULT_COORDINATE_PROXIMITY_TOLERANCE):
65
+ return self.coordinates_as_kdtree.query_pairs(r=tolerance)
66
+
67
+ @property
68
+ def number_of_atoms(self) -> int:
69
+ return len(self.elements.values)
70
+
36
71
  @classmethod
37
72
  def from_dict(
38
73
  cls,
@@ -80,9 +115,7 @@ class Basis(BasisSchema, InMemoryEntityPydantic):
80
115
  force: bool = False,
81
116
  ):
82
117
  """
83
- Add an atom to the basis.
84
-
85
- Before adding the atom at the specified coordinate, checks that no other atom is overlapping within a threshold.
118
+ Add an atom to the basis at a specified coordinate. Check that no other atom is overlapping with it.
86
119
 
87
120
  Args:
88
121
  element (str): Element symbol of the atom to be added.
@@ -128,15 +161,61 @@ class Basis(BasisSchema, InMemoryEntityPydantic):
128
161
  self.coordinates.remove_item(id)
129
162
  self.labels.remove_item(id)
130
163
 
131
- def filter_atoms_by_ids(self, ids: Union[List[int], int], invert: bool = False) -> "Basis":
132
- self.elements.filter_by_ids(ids, invert)
133
- self.coordinates.filter_by_ids(ids, invert)
134
- self.labels.filter_by_ids(ids, invert)
164
+ def remove_atoms_by_elements(self, values: Union[List[str], str]) -> "Basis":
165
+ if isinstance(values, str):
166
+ values = [values]
167
+ ids_to_remove = [
168
+ id_ for value in values for id_, v in zip(self.elements.ids, self.elements.values) if v == value
169
+ ]
170
+ self.filter_atoms_by_ids(ids_to_remove, invert=True)
171
+ return self
172
+
173
+ def filter_atoms_by_ids(self, ids: Union[List[int], int], invert: bool = False, reset_ids=False) -> "Basis":
174
+ self.elements.filter_by_ids(ids, invert, reset_ids=reset_ids)
175
+ self.coordinates.filter_by_ids(ids, invert, reset_ids=reset_ids)
176
+ self.labels.filter_by_ids(ids, invert, reset_ids=reset_ids)
135
177
  return self
136
178
 
137
179
  def filter_atoms_by_labels(self, labels: Union[List[str], str]) -> "Basis":
180
+ labels = [int(label) if isinstance(label, str) else label for label in labels]
138
181
  self.labels.filter_by_values(labels)
139
182
  ids = self.labels.ids
140
183
  self.elements.filter_by_ids(ids)
141
184
  self.coordinates.filter_by_ids(ids)
142
185
  return self
186
+
187
+ def set_labels_from_list(self, labels: List[Union[int, str]]) -> None:
188
+ num_atoms = len(self.elements.values)
189
+
190
+ if len(labels) != num_atoms:
191
+ raise ValueError(f"Number of labels ({len(labels)}) must match number of atoms ({num_atoms})")
192
+
193
+ self.labels = ArrayWithIds.from_values(values=list(labels))
194
+
195
+ def transform_by_matrix(self, matrix: Matrix3x3Schema) -> None:
196
+ original_is_in_crystal_units = self.is_in_crystal_units
197
+ self.to_crystal()
198
+ matrix_np = np.array(matrix)
199
+ new_coordinates = np.dot(self.coordinates.values, matrix_np)
200
+ self.coordinates.values = new_coordinates.tolist()
201
+ if not original_is_in_crystal_units:
202
+ self.to_cartesian()
203
+
204
+ # TODO: add/update test for this method
205
+ def resolve_colliding_coordinates(self, tolerance=DEFAULT_COORDINATE_PROXIMITY_TOLERANCE):
206
+ """
207
+ Find all atoms that are within distance tolerance and only keep the last one, remove other sites.
208
+
209
+ Args:
210
+ tolerance (float): The distance tolerance in angstroms.
211
+ """
212
+ original_is_in_crystal = self.is_in_crystal_units
213
+ self.to_cartesian()
214
+ ids_to_remove = set()
215
+ atom_ids = self.coordinates.ids
216
+ for index_1, index_2 in self.get_coordinates_colliding_pairs(tolerance):
217
+ ids_to_remove.add(atom_ids[index_1]) # Keep the last one in the pair
218
+
219
+ self.filter_atoms_by_ids(list(ids_to_remove), invert=True, reset_ids=True)
220
+ if original_is_in_crystal:
221
+ self.to_crystal()
@@ -3,14 +3,13 @@ from typing import List, Optional
3
3
 
4
4
  import numpy as np
5
5
  from mat3ra.code.entity import InMemoryEntityPydantic
6
- from mat3ra.esse.models.properties_directory.structural.lattice.lattice_bravais import (
7
- LatticeImplicitSchema as LatticeBravaisSchema,
8
- )
9
- from mat3ra.esse.models.properties_directory.structural.lattice.lattice_bravais import (
6
+ from mat3ra.esse.models.properties_directory.structural.lattice import (
7
+ LatticeSchema,
10
8
  LatticeTypeEnum,
11
9
  LatticeUnitsSchema,
12
10
  )
13
11
  from mat3ra.utils.mixins import RoundNumericValuesMixin
12
+ from pydantic import BaseModel
14
13
 
15
14
  from .cell import Cell
16
15
 
@@ -21,10 +20,23 @@ class LatticeVectors(Cell):
21
20
  pass
22
21
 
23
22
 
24
- class Lattice(RoundNumericValuesMixin, LatticeBravaisSchema, InMemoryEntityPydantic):
23
+ class LatticeSchemaVectorless(BaseModel):
24
+ """LatticeSchema without the vectors field to avoid conflicts."""
25
+
26
+ a: float
27
+ b: float
28
+ c: float
29
+ alpha: float
30
+ beta: float
31
+ gamma: float
32
+ units: LatticeUnitsSchema = LatticeSchema.model_fields["units"].default_factory()
33
+ type: LatticeTypeEnum = LatticeSchema.model_fields["type"].default
34
+
35
+
36
+ class Lattice(RoundNumericValuesMixin, LatticeSchemaVectorless, InMemoryEntityPydantic):
25
37
  __types__ = LatticeTypeEnum
26
- __type_default__ = LatticeBravaisSchema.model_fields["type"].default
27
- __units_default__ = LatticeBravaisSchema.model_fields["units"].default_factory()
38
+ __type_default__ = LatticeSchema.model_fields["type"].default
39
+ __units_default__ = LatticeSchema.model_fields["units"].default_factory()
28
40
 
29
41
  a: float = 1.0
30
42
  b: float = a
@@ -1,4 +1,4 @@
1
- from typing import Any, List
1
+ from typing import Any, List, Union
2
2
 
3
3
  from mat3ra.code.constants import AtomicCoordinateUnits, Units
4
4
  from mat3ra.code.entity import HasDescriptionHasMetadataNamedDefaultableInMemoryEntityPydantic
@@ -63,6 +63,12 @@ class Material(MaterialSchema, HasDescriptionHasMetadataNamedDefaultableInMemory
63
63
  self.name: str = self.formula
64
64
  self.basis.cell = self.lattice.vectors
65
65
 
66
+ @classmethod
67
+ def create_from_config_or_class_instance(cls, config_or_instance: Union[dict, "Material"]) -> "Material":
68
+ if isinstance(config_or_instance, cls):
69
+ return config_or_instance
70
+ return cls.create(config_or_instance)
71
+
66
72
  @property
67
73
  def coordinates_array(self) -> List[List[float]]:
68
74
  return self.basis.coordinates.values
@@ -76,7 +82,7 @@ class Material(MaterialSchema, HasDescriptionHasMetadataNamedDefaultableInMemory
76
82
  def set_coordinates(self, coordinates: List[List[float]]) -> None:
77
83
  self.basis.coordinates.values = coordinates
78
84
 
79
- def set_new_lattice_vectors(
85
+ def set_lattice_vectors(
80
86
  self, lattice_vector1: List[float], lattice_vector2: List[float], lattice_vector3: List[float]
81
87
  ) -> None:
82
88
  original_is_in_crystal_units = self.basis.is_in_crystal_units
@@ -86,8 +92,19 @@ class Material(MaterialSchema, HasDescriptionHasMetadataNamedDefaultableInMemory
86
92
  if original_is_in_crystal_units:
87
93
  self.to_crystal()
88
94
 
95
+ def set_lattice_vectors_from_array(self, lattice_vectors: List[List[float]]) -> None:
96
+ if len(lattice_vectors) != 3:
97
+ raise ValueError("Lattice vectors array must contain exactly three vectors.")
98
+ self.set_lattice_vectors(*lattice_vectors)
99
+
89
100
  def set_lattice(self, lattice: Lattice) -> None:
90
- self.set_new_lattice_vectors(*lattice.vector_arrays)
101
+ self.set_lattice_vectors(*lattice.vector_arrays)
91
102
 
92
103
  def add_atom(self, element: str, coordinate: List[float], use_cartesian_coordinates: bool = False) -> None:
93
104
  self.basis.add_atom(element, coordinate, use_cartesian_coordinates)
105
+
106
+ def set_labels_from_list(self, labels: List[Union[int, str]]) -> None:
107
+ self.basis.set_labels_from_list(labels)
108
+
109
+ def set_labels_from_value(self, value: Union[int, str]) -> None:
110
+ self.basis.set_labels_from_list([value] * self.basis.number_of_atoms)
@@ -0,0 +1,45 @@
1
+ import json
2
+ from typing import Any
3
+
4
+ from mat3ra.code.entity import InMemoryEntityPydantic
5
+ from pydantic import model_validator
6
+
7
+
8
+ def to_dict(obj: Any) -> dict:
9
+ if isinstance(obj, dict):
10
+ return obj
11
+ if hasattr(obj, "to_dict") and callable(obj.to_dict):
12
+ return obj.to_dict()
13
+ # TODO: remove conditional checks below when all configurations moved to Pydantic
14
+ if hasattr(obj, "to_json") and callable(obj.to_json):
15
+ data = obj.to_json()
16
+ if isinstance(data, str):
17
+ return json.loads(data)
18
+ return data
19
+ return {}
20
+
21
+
22
+ class BaseMetadata(InMemoryEntityPydantic):
23
+ model_config = {"arbitrary_types_allowed": True, "extra": "allow"}
24
+
25
+ @model_validator(mode="before")
26
+ def convert_fields_to_dict(cls, values):
27
+ for key, value in values.items():
28
+ field = cls.model_fields.get(key)
29
+ if field and field.annotation is dict and value is None:
30
+ values[key] = {}
31
+ elif field and field.annotation is dict and value is not None and not isinstance(value, dict):
32
+ values[key] = to_dict(value)
33
+ elif isinstance(value, InMemoryEntityPydantic):
34
+ values[key] = to_dict(value)
35
+ elif hasattr(value, "to_dict") and callable(value.to_dict):
36
+ values[key] = to_dict(value)
37
+ # TODO: remove when Pydantic configurations are fully implemented
38
+ elif hasattr(value, "to_json") and callable(value.to_json):
39
+ values[key] = to_dict(value)
40
+ return values
41
+
42
+ def update(self, **kwargs: Any):
43
+ for key, value in kwargs.items():
44
+ if hasattr(self, key) and isinstance(getattr(self, key), dict):
45
+ getattr(self, key).update(to_dict(value))
@@ -1,12 +1,17 @@
1
+ from typing import Union
2
+
1
3
  import numpy as np
2
4
  from mat3ra.made.material import Material
5
+ from pydantic import BaseModel
3
6
  from scipy.spatial.distance import pdist
4
7
 
8
+ from ..build import MaterialWithBuildMetadata
9
+ from .other import get_chemical_formula_empirical
10
+ from .utils import decorator_perform_operation_in_cartesian_coordinates
11
+
5
12
 
6
- class BaseMaterialAnalyzer:
7
- def __init__(self, material: Material):
8
- self.material = material.clone()
9
- self.material.to_cartesian()
13
+ class BaseMaterialAnalyzer(BaseModel):
14
+ material: Union[Material, MaterialWithBuildMetadata]
10
15
 
11
16
  @property
12
17
  def volume(self):
@@ -17,5 +22,10 @@ class BaseMaterialAnalyzer:
17
22
  return len(self.material.coordinates_array) / self.volume
18
23
 
19
24
  @property
25
+ @decorator_perform_operation_in_cartesian_coordinates
20
26
  def pairwise_distances(self):
21
27
  return pdist(np.array(self.material.coordinates_array))
28
+
29
+ @property
30
+ def formula(self):
31
+ return get_chemical_formula_empirical(self.material)
@@ -0,0 +1,75 @@
1
+ from typing import List
2
+
3
+ from mat3ra.esse.models.materials_category_components.entities.core.zero_dimensional.atom import AtomSchema
4
+ from mat3ra.made.material import Material
5
+ from mat3ra.made.tools.analyze.crystal_site import CrystalSiteAnalyzer
6
+ from mat3ra.made.tools.analyze.slab import SlabMaterialAnalyzer
7
+ from mat3ra.made.tools.build import MaterialWithBuildMetadata
8
+ from mat3ra.made.tools.build.defect.point.builders import AtomAtCoordinateBuilder, AtomAtCoordinateConfiguration
9
+ from mat3ra.made.tools.build.defect.slab.helpers import recreate_slab_with_fractional_layers
10
+ from mat3ra.made.tools.build.slab.builders import SlabBuilder
11
+ from mat3ra.made.tools.build.vacuum.builders import VacuumBuilder
12
+ from mat3ra.made.tools.build.vacuum.configuration import VacuumConfiguration
13
+
14
+
15
+ class AdatomMaterialAnalyzer(SlabMaterialAnalyzer):
16
+ distance_z: float
17
+ coordinate_2d: List[float] # Add coordinate property
18
+ element: str
19
+
20
+ @property
21
+ def added_component_height(self) -> float:
22
+ return self.layer_thickness
23
+
24
+ @property
25
+ def added_component_prototype(self) -> MaterialWithBuildMetadata:
26
+ vacuum_configuration = VacuumConfiguration(crystal=self.material, size=self.added_component_height)
27
+ vacuum_material = VacuumBuilder().get_material(vacuum_configuration)
28
+ return vacuum_material
29
+
30
+ @property
31
+ def coordinate_in_added_component(self) -> List[float]:
32
+ coordinate_3d = self.coordinate_2d + [0]
33
+ coordinate_3d_cartesian = self.material.basis.cell.convert_point_to_cartesian(coordinate_3d)
34
+ coordinate_3d_cartesian[2] = self.distance_z
35
+ coordinate_3d_crystal = self.added_component_prototype.basis.cell.convert_point_to_crystal(
36
+ coordinate_3d_cartesian
37
+ )
38
+ return coordinate_3d_crystal
39
+
40
+ @property
41
+ def added_component(self) -> MaterialWithBuildMetadata:
42
+ atom_configuration = AtomAtCoordinateConfiguration(
43
+ crystal=self.added_component_prototype,
44
+ element=AtomSchema(chemical_element=self.element),
45
+ coordinate=self.coordinate_in_added_component,
46
+ )
47
+ return AtomAtCoordinateBuilder().get_material(atom_configuration)
48
+
49
+ @property
50
+ def slab_material_or_configuration_for_stacking(self) -> MaterialWithBuildMetadata:
51
+ return self._slab_with_no_gap
52
+
53
+
54
+ class AdatomCrystalSiteMaterialAnalyzer(AdatomMaterialAnalyzer):
55
+ DEFAULT_NUMBER_OF_LAYERS: float = 1
56
+
57
+ @property
58
+ def added_component_prototype(self) -> Material:
59
+ # Recreate the slab with a single layer to ensure the adatom is placed correctly
60
+ return recreate_slab_with_fractional_layers(self.material, self.DEFAULT_NUMBER_OF_LAYERS)
61
+
62
+ @property
63
+ def coordinate_in_added_component(self) -> List[float]:
64
+ approximate_coordinate_3d = super().coordinate_in_added_component
65
+ crystal_site_analyzer = CrystalSiteAnalyzer(
66
+ material=self.added_component_prototype,
67
+ coordinate=approximate_coordinate_3d,
68
+ )
69
+ return crystal_site_analyzer.closest_site_coordinate
70
+
71
+ @property
72
+ def slab_material_or_configuration_for_stacking(self) -> MaterialWithBuildMetadata:
73
+ config = self.slab_configuration_with_no_vacuum
74
+ params = self.build_parameters
75
+ return SlabBuilder(build_parameters=params).get_material(config)
@@ -0,0 +1,108 @@
1
+ from typing import List
2
+
3
+ from ...utils import get_center_of_coordinates
4
+ from ..build.supercell import create_supercell
5
+ from ..convert import to_pymatgen
6
+ from ..modify import filter_by_condition_on_coordinates
7
+ from ..third_party import PymatgenVoronoiInterstitialGenerator
8
+ from ..utils import get_distance_between_coordinates, transform_coordinate_to_supercell
9
+ from . import BaseMaterialAnalyzer
10
+ from .coordination import get_voronoi_nearest_neighbors_atom_indices
11
+ from .other import get_closest_site_id_from_coordinate
12
+
13
+
14
+ class CrystalSiteAnalyzer(BaseMaterialAnalyzer):
15
+ coordinate: List[float] = [0.0, 0.0, 0.0]
16
+
17
+ @property
18
+ def exact_coordinate(self) -> List[float]:
19
+ return self.coordinate
20
+
21
+ @property
22
+ def closest_site_coordinate(self) -> List[float]:
23
+ site_id = get_closest_site_id_from_coordinate(self.material, self.coordinate)
24
+ return self.material.coordinates_array[site_id]
25
+
26
+ def get_equidistant_coordinate(self, coordinate=None) -> List[float]:
27
+ """
28
+ Compute a coordinate that is equidistant from the nearest atoms to the target coordinate. Useful for adatom.
29
+
30
+ This method works by:
31
+ 1. Creating a 3x3x1 supercell of the original material to include atoms in PBC.
32
+ 2. Transforming the target coordinate into the supercell's coordinate system.
33
+ 3. Using Voronoi tessellation to find the indices of the nearest atoms in the supercell.
34
+ 4. Taking the geometric center (average) of these neighboring atoms' coordinates as the equidistant point.
35
+ 5. Setting the z-coordinate to the original value (useful for 2D/slab systems).
36
+ 6. Transforming the equidistant coordinate back to the original cell's coordinate system.
37
+
38
+ The [3, 3, 1] supercell ensures robust neighbor search in x and y, but not in z (for 2D/slab systems).
39
+ """
40
+ if coordinate is None:
41
+ coordinate = self.coordinate
42
+ scaling_factor = [3, 3, 1]
43
+ translation_vector = [1 / 3, 1 / 3, 0]
44
+ supercell_material = create_supercell(self.material, scaling_factor=scaling_factor)
45
+
46
+ coordinate_in_supercell = transform_coordinate_to_supercell(
47
+ coordinate=coordinate, scaling_factor=scaling_factor, translation_vector=translation_vector
48
+ )
49
+
50
+ neighboring_atoms_ids_in_supercell = get_voronoi_nearest_neighbors_atom_indices(
51
+ material=supercell_material, coordinate=coordinate_in_supercell
52
+ )
53
+
54
+ if neighboring_atoms_ids_in_supercell is None:
55
+ raise ValueError("No neighboring atoms found for equidistant calculation.")
56
+
57
+ # Filter out atoms that are too close to the z boundaries of the supercell
58
+ supercell_material = filter_by_condition_on_coordinates(supercell_material, lambda c: 1e-2 < c[2] < 1 - 1e-2)
59
+ isolated_neighboring_atoms_basis = supercell_material.basis.model_copy()
60
+ isolated_neighboring_atoms_basis.coordinates.filter_by_ids(neighboring_atoms_ids_in_supercell)
61
+ equidistant_coordinate_in_supercell = get_center_of_coordinates(
62
+ isolated_neighboring_atoms_basis.coordinates.values
63
+ )
64
+
65
+ return transform_coordinate_to_supercell(
66
+ equidistant_coordinate_in_supercell, scaling_factor, translation_vector, reverse=True
67
+ )
68
+
69
+
70
+ class VoronoiCrystalSiteAnalyzer(CrystalSiteAnalyzer):
71
+ """
72
+ Attributes:
73
+ clustering_tol: Tolerance for clustering the Voronoi nodes.
74
+ min_dist: Minimum distance between an interstitial and the nearest atom.
75
+ ltol: Tolerance for lattice matching.
76
+ stol: Tolerance for structure matching.
77
+ angle_tol: Angle tolerance for structure matching.
78
+ """
79
+
80
+ clustering_tol: float = 0.5
81
+ min_dist: float = 0.9
82
+ ltol: float = 0.2
83
+ stol: float = 0.3
84
+ angle_tol: float = 5
85
+
86
+ @property
87
+ def voronoi_site_coordinate(self) -> List[float]:
88
+ pymatgen_structure = to_pymatgen(self.material)
89
+
90
+ voronoi_gen = PymatgenVoronoiInterstitialGenerator(
91
+ clustering_tol=self.clustering_tol,
92
+ min_dist=self.min_dist,
93
+ ltol=self.ltol,
94
+ stol=self.stol,
95
+ angle_tol=self.angle_tol,
96
+ )
97
+
98
+ interstitials = list(voronoi_gen.generate(structure=pymatgen_structure, insert_species=["Si"]))
99
+
100
+ if not interstitials:
101
+ raise ValueError("No Voronoi interstitial sites found.")
102
+
103
+ closest_interstitial = min(
104
+ interstitials,
105
+ key=lambda interstitial: get_distance_between_coordinates(interstitial.site.frac_coords, self.coordinate),
106
+ )
107
+
108
+ return closest_interstitial.site.frac_coords.tolist()
@@ -0,0 +1,24 @@
1
+ from mat3ra.made.tools.analyze.interface.commensurate import (
2
+ CommensurateLatticeInterfaceAnalyzer,
3
+ CommensurateLatticeMatchHolder,
4
+ )
5
+ from mat3ra.made.tools.analyze.interface.grain_boundary import (
6
+ GrainBoundaryPlanarAnalyzer,
7
+ GrainBoundaryPlanarMatchHolder,
8
+ )
9
+ from mat3ra.made.tools.analyze.interface.simple import InterfaceAnalyzer
10
+ from mat3ra.made.tools.analyze.interface.twisted_nanoribbons import TwistedNanoribbonsInterfaceAnalyzer
11
+ from mat3ra.made.tools.analyze.interface.utils.holders import MatchedSubstrateFilmConfigurationHolder
12
+ from mat3ra.made.tools.analyze.interface.zsl import ZSLInterfaceAnalyzer, ZSLMatchHolder
13
+
14
+ __all__ = [
15
+ "InterfaceAnalyzer",
16
+ "ZSLInterfaceAnalyzer",
17
+ "ZSLMatchHolder",
18
+ "CommensurateLatticeInterfaceAnalyzer",
19
+ "CommensurateLatticeMatchHolder",
20
+ "GrainBoundaryPlanarAnalyzer",
21
+ "GrainBoundaryPlanarMatchHolder",
22
+ "TwistedNanoribbonsInterfaceAnalyzer",
23
+ "MatchedSubstrateFilmConfigurationHolder",
24
+ ]