diff --git a/other/materials_designer/workflows/interfacial_energy.ipynb b/other/materials_designer/workflows/interfacial_energy.ipynb index f265de0d..2ae66a25 100644 --- a/other/materials_designer/workflows/interfacial_energy.ipynb +++ b/other/materials_designer/workflows/interfacial_energy.ipynb @@ -618,7 +618,7 @@ "visualize_properties(interfacial_energy_data, title=\"Interfacial Energy\")\n", "\n", "print(f\"Interface (material 0): {saved_interface.name} ({saved_interface.id}), \"\n", - " f\"total energy: {interface_te_data[0]['data']['value']} eV\")\n", + " f\"total energy: {interface_te_data[0]['value']} eV\")\n", "print(f\"Substrate bulk (material 1): {substrate_bulk.name} ({substrate_bulk.id}), \"\n", " f\"total energy: {substrate_te_property['data']['value']} eV\")\n", "print(f\"Film bulk (material 2): {film_bulk.name} ({film_bulk.id}), \"\n", diff --git a/other/materials_designer/workflows/surface_energy.ipynb b/other/materials_designer/workflows/surface_energy.ipynb index e98e13fc..ac9a1899 100644 --- a/other/materials_designer/workflows/surface_energy.ipynb +++ b/other/materials_designer/workflows/surface_energy.ipynb @@ -526,7 +526,7 @@ "visualize_properties(surface_energy_data, title=\"Surface Energy\")\n", "\n", "print(f\"Slab: {saved_slab.name} ({saved_slab.id}), \"\n", - " f\"total energy: {slab_total_energy_data[0]['data']['value']} eV\")\n", + " f\"total energy: {slab_total_energy_data[0]['value']} eV\")\n", "print(f\"Bulk: {bulk_material.name} ({bulk_material.id}), \"\n", " f\"total energy: {bulk_total_energy_property['data']['value']} eV\")" ] diff --git a/src/py/mat3ra/notebooks_utils/core/entity/material/analysis.py b/src/py/mat3ra/notebooks_utils/core/entity/material/analysis.py index 78424dd0..bd329339 100644 --- a/src/py/mat3ra/notebooks_utils/core/entity/material/analysis.py +++ b/src/py/mat3ra/notebooks_utils/core/entity/material/analysis.py @@ -120,7 +120,7 @@ def get_slab_bulk_crystal(slab_material: Material) -> dict: def resolve_bulk_query_from_crystal(bulk_crystal: dict) -> dict: """Builds a materials.list query that resolves a bulk crystal to a platform material.""" - for key in ("scaledHash", "hash", "_id"): + for key in ("_id", "hash", "scaledHash"): if bulk_crystal.get(key) is not None: return {key: bulk_crystal[key]} try: diff --git a/src/py/mat3ra/notebooks_utils/core/entity/material/api.py b/src/py/mat3ra/notebooks_utils/core/entity/material/api.py index 97be0959..287beb0c 100644 --- a/src/py/mat3ra/notebooks_utils/core/entity/material/api.py +++ b/src/py/mat3ra/notebooks_utils/core/entity/material/api.py @@ -70,8 +70,8 @@ def get_bulk_material_by_crystal(api_client: APIClient, bulk_crystal: Material, def _require_material_for_owner(api_client: APIClient, query: dict, owner_id: str) -> Material: - matches = api_client.materials.list(query) - material_response = next((item for item in matches if item.get("owner", {}).get("_id") == owner_id), None) + matches = api_client.materials.list({**query, "owner._id": owner_id}) + material_response = next(iter(matches), None) if material_response is None: raise ValueError( "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 that already have one do not re-query for it. """ material_set_id = material_set["_id"] - matches = api_client.materials.list({"owner._id": owner_id, "inSet._id": material_set_id}) + matches = api_client.materials.list( + {"owner._id": owner_id, "inSet._id": material_set_id, "isEntitySet": {"$ne": True}} + ) members = [material for material in matches if not material.get("isEntitySet")] return sorted(members, key=lambda material: _index_in_set(material, material_set_id)) diff --git a/src/py/mat3ra/notebooks_utils/core/entity/property/api.py b/src/py/mat3ra/notebooks_utils/core/entity/property/api.py index f57f538e..050c16f4 100644 --- a/src/py/mat3ra/notebooks_utils/core/entity/property/api.py +++ b/src/py/mat3ra/notebooks_utils/core/entity/property/api.py @@ -71,7 +71,7 @@ def update_property_holder_value(client: APIClient, property_holder_id: str, val return client.properties.update(property_holder_id, {"$set": {"data.value": value}}) -def find_total_energy_for_material(client: APIClient, material_id: str, source: str = "public") -> Optional[dict]: +def find_total_energy_for_material(client: APIClient, material_id: str, source: str = "my_account") -> Optional[dict]: """ Find the best-precision total_energy property for a material. Mirrors the 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: Args: client (APIClient): API client instance. material_id (str): Material _id to look up the total_energy property for. - source (str): Source of the total energy property: `public`, `my_account` or `curators`. + source (str): Source of the total energy property: `my_account` (default), `curators` or + `public`. Returns: The best-precision total_energy property, or None if none exists. diff --git a/tests/py/unit/core/entity/test_material_analysis.py b/tests/py/unit/core/entity/test_material_analysis.py index 28e43311..ac99995f 100644 --- a/tests/py/unit/core/entity/test_material_analysis.py +++ b/tests/py/unit/core/entity/test_material_analysis.py @@ -1,7 +1,10 @@ """Unit tests for bulk-crystal query resolution.""" import pytest -from mat3ra.notebooks_utils.core.entity.material.analysis import resolve_bulk_query_from_crystal +from mat3ra.made.material import Material +from mat3ra.made.tools.analyze.lattice_planes import CrystalLatticePlanesMaterialAnalyzer +from mat3ra.made.tools.helpers import create_slab +from mat3ra.notebooks_utils.core.entity.material.analysis import get_slab_bulk_crystal, resolve_bulk_query_from_crystal from mat3ra.standata.materials import Materials SILICON = Materials.get_by_name_first_match("Silicon") @@ -12,13 +15,13 @@ [ ( {"scaledHash": "scaled-hash-value", "hash": "hash-value", "_id": "material-id"}, - {"scaledHash": "scaled-hash-value"}, + {"_id": "material-id"}, ), - ({"hash": "hash-value", "_id": "material-id"}, {"hash": "hash-value"}), - ({"_id": "material-id"}, {"_id": "material-id"}), + ({"scaledHash": "scaled-hash-value", "hash": "hash-value"}, {"hash": "hash-value"}), + ({"scaledHash": "scaled-hash-value"}, {"scaledHash": "scaled-hash-value"}), ], ) -def test_resolve_bulk_query_prefers_scaled_hash_then_hash_then_id(extra_keys, expected): +def test_resolve_bulk_query_prefers_id_then_hash_then_scaled_hash(extra_keys, expected): assert resolve_bulk_query_from_crystal({**SILICON, **extra_keys}) == expected @@ -26,3 +29,20 @@ def test_resolve_bulk_query_computes_hash_when_none_present(): query = resolve_bulk_query_from_crystal(SILICON) assert set(query) == {"hash"} assert query["hash"] + + +def test_slab_bulk_crystal_is_the_material_the_slab_was_built_from(): + """ + A slab built from a primitive bulk must resolve back to that primitive bulk, not to the + conventional cell -- otherwise the Total Energy job run on the input is unreachable and + surface energy combines a slab SCF with a bulk reference from a different cell. + """ + primitive = Material.create(Materials.get_by_name_first_match("Nickel")) + conventional = CrystalLatticePlanesMaterialAnalyzer( + material=primitive, miller_indices=(0, 0, 1) + ).material_with_conventional_lattice + assert conventional.hash != primitive.hash + + slab = create_slab(crystal=primitive, miller_indices=(0, 0, 1), number_of_layers=3) + + assert get_slab_bulk_crystal(slab)["hash"] == primitive.hash diff --git a/tests/py/unit/core/entity/test_material_api.py b/tests/py/unit/core/entity/test_material_api.py index 4325d74e..b1cfbebb 100644 --- a/tests/py/unit/core/entity/test_material_api.py +++ b/tests/py/unit/core/entity/test_material_api.py @@ -100,7 +100,9 @@ def test_list_materials_in_set_does_not_re_resolve_the_set(): materials = list_materials_in_set(client, OWNER_ID, ENTITY_SET) assert [material["_id"] for material in materials] == EXPECTED_ORDERED_IDS - client.materials.list.assert_called_once_with({"owner._id": OWNER_ID, "inSet._id": MATERIAL_SET_ID}) + client.materials.list.assert_called_once_with( + {"owner._id": OWNER_ID, "inSet._id": MATERIAL_SET_ID, "isEntitySet": {"$ne": True}} + ) @pytest.mark.parametrize( @@ -119,6 +121,7 @@ def test_list_materials_by_set_orders_by_inset_index(members, expected_ids): assert client.materials.list.call_args_list[1].args[0] == { "owner._id": OWNER_ID, "inSet._id": MATERIAL_SET_ID, + "isEntitySet": {"$ne": True}, } diff --git a/tests/py/unit/core/entity/test_property_api_find_total_energy_for_material.py b/tests/py/unit/core/entity/test_property_api_find_total_energy_for_material.py index 3389698d..a7596ca7 100644 --- a/tests/py/unit/core/entity/test_property_api_find_total_energy_for_material.py +++ b/tests/py/unit/core/entity/test_property_api_find_total_energy_for_material.py @@ -22,7 +22,7 @@ def _client(): return client -def test_find_total_energy_for_material_defaults_to_public_scope(): +def test_find_total_energy_for_material_defaults_to_my_account_scope(): client = _client() result = find_total_energy_for_material(client, MATERIAL_ID) @@ -32,6 +32,7 @@ def test_find_total_energy_for_material_defaults_to_public_scope(): query={ "exabyteId": EXABYTE_ID, "slug": "total_energy", + "owner._id": OWNER_ACCOUNT_ID, }, projection={"sort": {"precision.value": -1}, "limit": 1}, )