Skip to content

Commit adec4d7

Browse files
authored
Merge pull request #361 from mat3ra/feature/SOF-8034
feature/SOF-8034 Update: assert a slab resolves back to its own bulk
2 parents 6c9598e + b66f6a3 commit adec4d7

8 files changed

Lines changed: 42 additions & 15 deletions

File tree

other/materials_designer/workflows/interfacial_energy.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@
618618
"visualize_properties(interfacial_energy_data, title=\"Interfacial Energy\")\n",
619619
"\n",
620620
"print(f\"Interface (material 0): {saved_interface.name} ({saved_interface.id}), \"\n",
621-
" f\"total energy: {interface_te_data[0]['data']['value']} eV\")\n",
621+
" f\"total energy: {interface_te_data[0]['value']} eV\")\n",
622622
"print(f\"Substrate bulk (material 1): {substrate_bulk.name} ({substrate_bulk.id}), \"\n",
623623
" f\"total energy: {substrate_te_property['data']['value']} eV\")\n",
624624
"print(f\"Film bulk (material 2): {film_bulk.name} ({film_bulk.id}), \"\n",

other/materials_designer/workflows/surface_energy.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@
526526
"visualize_properties(surface_energy_data, title=\"Surface Energy\")\n",
527527
"\n",
528528
"print(f\"Slab: {saved_slab.name} ({saved_slab.id}), \"\n",
529-
" f\"total energy: {slab_total_energy_data[0]['data']['value']} eV\")\n",
529+
" f\"total energy: {slab_total_energy_data[0]['value']} eV\")\n",
530530
"print(f\"Bulk: {bulk_material.name} ({bulk_material.id}), \"\n",
531531
" f\"total energy: {bulk_total_energy_property['data']['value']} eV\")"
532532
]

src/py/mat3ra/notebooks_utils/core/entity/material/analysis.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def get_slab_bulk_crystal(slab_material: Material) -> dict:
120120

121121
def resolve_bulk_query_from_crystal(bulk_crystal: dict) -> dict:
122122
"""Builds a materials.list query that resolves a bulk crystal to a platform material."""
123-
for key in ("scaledHash", "hash", "_id"):
123+
for key in ("_id", "hash", "scaledHash"):
124124
if bulk_crystal.get(key) is not None:
125125
return {key: bulk_crystal[key]}
126126
try:

src/py/mat3ra/notebooks_utils/core/entity/material/api.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ def get_bulk_material_by_crystal(api_client: APIClient, bulk_crystal: Material,
7070

7171

7272
def _require_material_for_owner(api_client: APIClient, query: dict, owner_id: str) -> Material:
73-
matches = api_client.materials.list(query)
74-
material_response = next((item for item in matches if item.get("owner", {}).get("_id") == owner_id), None)
73+
matches = api_client.materials.list({**query, "owner._id": owner_id})
74+
material_response = next(iter(matches), None)
7575
if material_response is None:
7676
raise ValueError(
7777
"The bulk material resolved from metadata is not present on the platform for this account. "
@@ -142,7 +142,9 @@ def list_materials_in_set(api_client: APIClient, owner_id: str, material_set: Di
142142
that already have one do not re-query for it.
143143
"""
144144
material_set_id = material_set["_id"]
145-
matches = api_client.materials.list({"owner._id": owner_id, "inSet._id": material_set_id})
145+
matches = api_client.materials.list(
146+
{"owner._id": owner_id, "inSet._id": material_set_id, "isEntitySet": {"$ne": True}}
147+
)
146148
members = [material for material in matches if not material.get("isEntitySet")]
147149
return sorted(members, key=lambda material: _index_in_set(material, material_set_id))
148150

src/py/mat3ra/notebooks_utils/core/entity/property/api.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def update_property_holder_value(client: APIClient, property_holder_id: str, val
7171
return client.properties.update(property_holder_id, {"$set": {"data.value": value}})
7272

7373

74-
def find_total_energy_for_material(client: APIClient, material_id: str, source: str = "public") -> Optional[dict]:
74+
def find_total_energy_for_material(client: APIClient, material_id: str, source: str = "my_account") -> Optional[dict]:
7575
"""
7676
Find the best-precision total_energy property for a material. Mirrors the
7777
platform's "Resolve Total Energies for Elemental Materials" subworkflow,
@@ -84,7 +84,8 @@ def find_total_energy_for_material(client: APIClient, material_id: str, source:
8484
Args:
8585
client (APIClient): API client instance.
8686
material_id (str): Material _id to look up the total_energy property for.
87-
source (str): Source of the total energy property: `public`, `my_account` or `curators`.
87+
source (str): Source of the total energy property: `my_account` (default), `curators` or
88+
`public`.
8889
8990
Returns:
9091
The best-precision total_energy property, or None if none exists.
Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"""Unit tests for bulk-crystal query resolution."""
22

33
import pytest
4-
from mat3ra.notebooks_utils.core.entity.material.analysis import resolve_bulk_query_from_crystal
4+
from mat3ra.made.material import Material
5+
from mat3ra.made.tools.analyze.lattice_planes import CrystalLatticePlanesMaterialAnalyzer
6+
from mat3ra.made.tools.helpers import create_slab
7+
from mat3ra.notebooks_utils.core.entity.material.analysis import get_slab_bulk_crystal, resolve_bulk_query_from_crystal
58
from mat3ra.standata.materials import Materials
69

710
SILICON = Materials.get_by_name_first_match("Silicon")
@@ -12,17 +15,34 @@
1215
[
1316
(
1417
{"scaledHash": "scaled-hash-value", "hash": "hash-value", "_id": "material-id"},
15-
{"scaledHash": "scaled-hash-value"},
18+
{"_id": "material-id"},
1619
),
17-
({"hash": "hash-value", "_id": "material-id"}, {"hash": "hash-value"}),
18-
({"_id": "material-id"}, {"_id": "material-id"}),
20+
({"scaledHash": "scaled-hash-value", "hash": "hash-value"}, {"hash": "hash-value"}),
21+
({"scaledHash": "scaled-hash-value"}, {"scaledHash": "scaled-hash-value"}),
1922
],
2023
)
21-
def test_resolve_bulk_query_prefers_scaled_hash_then_hash_then_id(extra_keys, expected):
24+
def test_resolve_bulk_query_prefers_id_then_hash_then_scaled_hash(extra_keys, expected):
2225
assert resolve_bulk_query_from_crystal({**SILICON, **extra_keys}) == expected
2326

2427

2528
def test_resolve_bulk_query_computes_hash_when_none_present():
2629
query = resolve_bulk_query_from_crystal(SILICON)
2730
assert set(query) == {"hash"}
2831
assert query["hash"]
32+
33+
34+
def test_slab_bulk_crystal_is_the_material_the_slab_was_built_from():
35+
"""
36+
A slab built from a primitive bulk must resolve back to that primitive bulk, not to the
37+
conventional cell -- otherwise the Total Energy job run on the input is unreachable and
38+
surface energy combines a slab SCF with a bulk reference from a different cell.
39+
"""
40+
primitive = Material.create(Materials.get_by_name_first_match("Nickel"))
41+
conventional = CrystalLatticePlanesMaterialAnalyzer(
42+
material=primitive, miller_indices=(0, 0, 1)
43+
).material_with_conventional_lattice
44+
assert conventional.hash != primitive.hash
45+
46+
slab = create_slab(crystal=primitive, miller_indices=(0, 0, 1), number_of_layers=3)
47+
48+
assert get_slab_bulk_crystal(slab)["hash"] == primitive.hash

tests/py/unit/core/entity/test_material_api.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ def test_list_materials_in_set_does_not_re_resolve_the_set():
100100
materials = list_materials_in_set(client, OWNER_ID, ENTITY_SET)
101101

102102
assert [material["_id"] for material in materials] == EXPECTED_ORDERED_IDS
103-
client.materials.list.assert_called_once_with({"owner._id": OWNER_ID, "inSet._id": MATERIAL_SET_ID})
103+
client.materials.list.assert_called_once_with(
104+
{"owner._id": OWNER_ID, "inSet._id": MATERIAL_SET_ID, "isEntitySet": {"$ne": True}}
105+
)
104106

105107

106108
@pytest.mark.parametrize(
@@ -119,6 +121,7 @@ def test_list_materials_by_set_orders_by_inset_index(members, expected_ids):
119121
assert client.materials.list.call_args_list[1].args[0] == {
120122
"owner._id": OWNER_ID,
121123
"inSet._id": MATERIAL_SET_ID,
124+
"isEntitySet": {"$ne": True},
122125
}
123126

124127

tests/py/unit/core/entity/test_property_api_find_total_energy_for_material.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def _client():
2222
return client
2323

2424

25-
def test_find_total_energy_for_material_defaults_to_public_scope():
25+
def test_find_total_energy_for_material_defaults_to_my_account_scope():
2626
client = _client()
2727

2828
result = find_total_energy_for_material(client, MATERIAL_ID)
@@ -32,6 +32,7 @@ def test_find_total_energy_for_material_defaults_to_public_scope():
3232
query={
3333
"exabyteId": EXABYTE_ID,
3434
"slug": "total_energy",
35+
"owner._id": OWNER_ACCOUNT_ID,
3536
},
3637
projection={"sort": {"precision.value": -1}, "limit": 1},
3738
)

0 commit comments

Comments
 (0)