@mat3ra/made 2026.5.21-1 → 2026.5.21-2

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": "2026.5.21-1",
3
+ "version": "2026.5.21-2",
4
4
  "description": "MAterials DEsign library",
5
5
  "scripts": {
6
6
  "lint": "eslint --cache src/js tests/js && prettier --write src/js tests/js",
@@ -132,6 +132,7 @@ class Basis(BasisSchema, InMemoryEntityPydantic):
132
132
  coordinate: Optional[List[float]] = None,
133
133
  use_cartesian_coordinates: bool = False,
134
134
  force: bool = False,
135
+ label: Optional[Union[int, str]] = None,
135
136
  ):
136
137
  """
137
138
  Add an atom to the basis at a specified coordinate. Check that no other atom is overlapping with it.
@@ -141,6 +142,7 @@ class Basis(BasisSchema, InMemoryEntityPydantic):
141
142
  coordinate (List[float]): Coordinate of the atom to be added.
142
143
  use_cartesian_coordinates (bool): Whether the coordinate is in Cartesian units (or crystal by default).
143
144
  force (bool): Whether to force adding the atom even if it overlaps with another atom.
145
+ label (int|str|None): Per-atom label when the basis already uses labels; omit if the basis has none.
144
146
  """
145
147
  if coordinate is None:
146
148
  coordinate = [0, 0, 0]
@@ -162,6 +164,8 @@ class Basis(BasisSchema, InMemoryEntityPydantic):
162
164
  return
163
165
  self.elements.add_item(element)
164
166
  self.coordinates.add_item(coordinate)
167
+ if label is not None:
168
+ self.labels.add_item(label)
165
169
 
166
170
  def add_atoms_from_another_basis(self, other_basis: "Basis"):
167
171
  """
@@ -7,6 +7,7 @@ from mat3ra.esse.models.materials_category_components.entities.core.zero_dimensi
7
7
  from mat3ra.esse.models.materials_category_components.operations.core.combinations.merge import MergeMethodsEnum
8
8
 
9
9
  from mat3ra.made.material import Material
10
+ from mat3ra.made.tools.analyze.other import get_closest_site_id_from_coordinate
10
11
  from ..base.configuration import PointDefectConfiguration
11
12
  from ......build_components import MaterialWithBuildMetadata
12
13
  from ......build_components.entities.auxiliary.zero_dimensional.point_defect_site.configuration import (
@@ -14,6 +15,16 @@ from ......build_components.entities.auxiliary.zero_dimensional.point_defect_sit
14
15
  )
15
16
 
16
17
 
18
+ def _host_atom_label_at_coordinate(
19
+ crystal: Union[Material, MaterialWithBuildMetadata], coordinate: List[float]
20
+ ) -> Union[int, str, None]:
21
+ host_labels = crystal.basis.labels.values
22
+ if not host_labels:
23
+ return None
24
+ site_index = get_closest_site_id_from_coordinate(crystal, coordinate)
25
+ return host_labels[site_index]
26
+
27
+
17
28
  class SubstitutionalDefectConfiguration(PointDefectConfiguration, SubstitutionalPointDefectSchema):
18
29
  type: str = "SubstitutionalDefectConfiguration"
19
30
 
@@ -25,5 +36,6 @@ class SubstitutionalDefectConfiguration(PointDefectConfiguration, Substitutional
25
36
  crystal=crystal,
26
37
  element=AtomSchema(chemical_element=element),
27
38
  coordinate=coordinate,
39
+ host_atom_label=_host_atom_label_at_coordinate(crystal, coordinate),
28
40
  )
29
41
  return cls(merge_components=[crystal, substitution_site], merge_method=MergeMethodsEnum.REPLACE, **kwargs)
@@ -26,5 +26,6 @@ class PointDefectSiteBuilder(BaseSingleBuilder):
26
26
  new_material.basis.add_atom(
27
27
  element=configuration.element.chemical_element,
28
28
  coordinate=configuration.coordinate,
29
+ label=configuration.host_atom_label,
29
30
  )
30
31
  return new_material
@@ -1,4 +1,4 @@
1
- from typing import Union
1
+ from typing import Optional, Union
2
2
 
3
3
  from mat3ra.esse.models.materials_category.defective_structures.two_dimensional.adatom.configuration import (
4
4
  PointDefectSiteSchema,
@@ -11,3 +11,4 @@ from ..crystal_site import CrystalSite
11
11
 
12
12
  class PointDefectSiteConfiguration(CrystalSite, PointDefectSiteSchema):
13
13
  element: Union[VacancySchema, AtomSchema]
14
+ host_atom_label: Optional[Union[int, str]] = None
@@ -16,6 +16,7 @@ from mat3ra.made.tools.build.defective_structures.zero_dimensional.point_defect.
16
16
  )
17
17
  from mat3ra.made.tools.build.defective_structures.zero_dimensional.point_defect.types import PointDefectDict
18
18
  from unit.fixtures.bulk import BULK_Si_CONVENTIONAL, BULK_Si_PRIMITIVE
19
+ from unit.fixtures.interface.zsl import GRAPHENE_NICKEL_INTERFACE
19
20
  from unit.fixtures.point_defects import (
20
21
  INTERSTITIAL_DEFECT_BULK_PRIMITIVE_Si,
21
22
  INTERSTITIAL_VORONOI_DEFECT_BULK_PRIMITIVE_Si,
@@ -25,6 +26,19 @@ from unit.fixtures.point_defects import (
25
26
  )
26
27
  from unit.utils import assert_two_entities_deep_almost_equal, get_platform_specific_value
27
28
 
29
+ FILM_LABEL = 1
30
+ SUBSTRATE_LABEL = 0
31
+ SUBSTITUTION_ELEMENT = "Ge"
32
+ INTERFACE_SUBSTITUTION_PLACEMENT_METHOD = SubstitutionPlacementMethodEnum.CLOSEST_SITE.value
33
+
34
+
35
+ def _film_atom_coordinate(material: Material) -> list:
36
+ labels_by_id = {entry["id"]: entry["value"] for entry in material.basis.labels.to_dict()}
37
+ for coordinate_entry in material.basis.coordinates.to_dict():
38
+ if labels_by_id.get(coordinate_entry["id"]) == FILM_LABEL:
39
+ return coordinate_entry["value"]
40
+ raise ValueError("No film atom found in fixture material")
41
+
28
42
 
29
43
  @pytest.mark.parametrize(
30
44
  "material_config, defect_params, expected_material_config",
@@ -91,6 +105,25 @@ def test_point_defect_helpers(material_config, defect_params, expected_material_
91
105
  assert_two_entities_deep_almost_equal(defect, expected_material_config)
92
106
 
93
107
 
108
+ @pytest.mark.parametrize(
109
+ "expected_label_count, expected_unique_labels",
110
+ [(5, {SUBSTRATE_LABEL, FILM_LABEL})],
111
+ )
112
+ def test_create_defect_point_substitution_preserves_interface_labels(
113
+ expected_label_count, expected_unique_labels
114
+ ):
115
+ material = Material.create(GRAPHENE_NICKEL_INTERFACE)
116
+ coordinate = _film_atom_coordinate(material)
117
+
118
+ defect_material = create_defect_point_substitution(
119
+ material, coordinate, SUBSTITUTION_ELEMENT, INTERFACE_SUBSTITUTION_PLACEMENT_METHOD
120
+ )
121
+
122
+ assert len(defect_material.basis.elements.values) == expected_label_count
123
+ assert len(defect_material.basis.labels.values) == expected_label_count
124
+ assert set(defect_material.basis.labels.values) == expected_unique_labels
125
+
126
+
94
127
  @pytest.mark.parametrize(
95
128
  "material_config, defect_params_list, expected_material_config",
96
129
  [