From 4e5326104bc75e4565274f786bca3d0eb3b36811 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 24 Aug 2026 19:39:51 -0700 Subject: [PATCH 01/20] Update: assert a slab resolves back to its own bulk [SOF-8034] The existing tests here mock crystal dicts to check query precedence and pass whichever cell made records, so nothing in this repo could tell the SOF-8034 defect from correct behaviour. Build a slab from a primitive bulk and assert get_slab_bulk_crystal resolves back to that same cell. Red against made before the fix (resolves the conventional Ni4), green after. Co-Authored-By: Claude Opus 5 (1M context) --- .../core/entity/test_material_analysis.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/py/unit/core/entity/test_material_analysis.py b/tests/py/unit/core/entity/test_material_analysis.py index 28e43311..00d0c228 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") @@ -26,3 +29,22 @@ 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 + # Without this the test would pass against broken code if the standata entry ever became the + # conventional cell -- the two hashes have to differ for the assertion below to mean anything. + 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 From 81cae341b24bf7b9d0c0e6f49975f862273aed59 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 24 Aug 2026 21:06:27 -0700 Subject: [PATCH 02/20] Chore: consume the made WIP wheel until made#297 releases [SOF-8034] api-examples resolves mat3ra-made from PyPI, and made publishes there only from main, so the new test could not pass on CI before made#297 merges. Point at the pre-release wheel that build-wheel.yml published from that branch, the same way config.yml has always consumed a GitHub Pages wheel. TEMPORARY. Revert to the bare "mat3ra-made" once made#297 merges and publishes. Co-Authored-By: Claude Opus 5 (1M context) --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bd6adf9b..2ad655f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,9 @@ materials = [ "mat3ra-notebooks-utils[utils_standata]", "pymatgen==2024.4.13", "pymatgen-analysis-defects<=2024.4.23", - "mat3ra-made", + # TEMPORARY [SOF-8034]: WIP wheel built from made#297 by build-wheel.yml. Revert to the + # bare "mat3ra-made" once made#297 merges and publishes to PyPI. + "mat3ra-made @ https://mat3ra.github.io/made/mat3ra_made-0.1.dev1+g536f0ff28-py3-none-any.whl", "mat3ra-periodic-table" ] workflows = [ From bbd46e190f0ddc71a91bd87d7fe4b908984ee599 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 24 Aug 2026 21:13:02 -0700 Subject: [PATCH 03/20] Chore: install the made WIP wheel in JupyterLite too [SOF-8034] config.yml is what JupyterLite micropips at runtime, so the notebooks kept getting the PyPI made even with pyproject pinned. Point the `made` profile at the same pre-release wheel, using the GitHub Pages form the file already documents just below. TEMPORARY. Revert to `- mat3ra-made` once made#297 merges and publishes. Co-Authored-By: Claude Opus 5 (1M context) --- config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config.yml b/config.yml index 5a5c5477..784dec57 100644 --- a/config.yml +++ b/config.yml @@ -29,7 +29,8 @@ notebooks: - emfs:/drive/packages/pydantic-2.7.1-py3-none-any.whl - pymatgen-analysis-defects<=2024.4.23 - mat3ra-periodic-table - - mat3ra-made + # TEMPORARY [SOF-8034]: WIP wheel from made#297. Revert to `- mat3ra-made` on release. + - https://mat3ra.github.io/made/mat3ra_made-0.1.dev1+g536f0ff28-py3-none-any.whl # packages below are used when made is installed from GH wheel (made should be installed from below) # - https://exabyte-io.github.io/made/mat3ra_made-0.1.dev1+ge64d360b8-py3-none-any.whl # - mat3ra-code From 9a1aea7cd8daf7b7c473bc76a9ab4c35b8d77331 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 24 Aug 2026 23:26:46 -0700 Subject: [PATCH 04/20] Fix: resolve a bulk material with the owner in the query [SOF-8034] _require_material_for_owner queried by hash alone and then filtered the response by owner. The server truncates that response, so for a common cell the page comes back full of other accounts' hash twins and the account's own material is absent from it -- reported as 'not present on the platform for this account' while it sits right there. Measured against seminar-2018-05: a scaledHash query for Cu mp-30 returned 20 materials across 13 other owners and none of the account's own, though the material resolves instantly once owner._id is part of the query. get_or_create_material two functions above already queries this way. Co-Authored-By: Claude Opus 5 (1M context) --- src/py/mat3ra/notebooks_utils/core/entity/material/api.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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..a480192a 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,10 @@ 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) + # Owner belongs in the query, not in a filter over the response: the server truncates, and the + # account's own material is routinely absent from a page full of other owners' hash twins. + 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. " From aa51b596db7a56f8bc2f55c5dd4e34fc8d12d0b4 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 24 Aug 2026 23:42:57 -0700 Subject: [PATCH 05/20] Fix: query set membership instead of filtering the response [SOF-8034] Same hazard as the owner fix: the server truncates, so filtering isEntitySet out of the response can drop real members along with the set document. Put it in the query. Co-Authored-By: Claude Opus 5 (1M context) --- src/py/mat3ra/notebooks_utils/core/entity/material/api.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 a480192a..ab725991 100644 --- a/src/py/mat3ra/notebooks_utils/core/entity/material/api.py +++ b/src/py/mat3ra/notebooks_utils/core/entity/material/api.py @@ -144,8 +144,11 @@ 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}) - members = [material for material in matches if not material.get("isEntitySet")] + # isEntitySet belongs in the query for the same reason owner does: the server truncates, so a + # response-side filter can drop real members along with the set document. + members = api_client.materials.list( + {"owner._id": owner_id, "inSet._id": material_set_id, "isEntitySet": {"$ne": True}} + ) return sorted(members, key=lambda material: _index_in_set(material, material_set_id)) From 01827b75d71b35e4cd84cf51086b591c6b9d31df Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 24 Aug 2026 23:45:15 -0700 Subject: [PATCH 06/20] Fix: keep the local isEntitySet guard, and pin the new query shape [SOF-8034] aa51b596 moved isEntitySet into the query but dropped the response-side filter, which broke three tests: their fakes return the set document regardless of query, so it leaked into the members list. Keep both. The query narrows server-side, which is the point -- the server truncates. The local filter stays as a guard because this platform has been observed ignoring list queries, and it costs nothing. The two tests that pin the exact query dict are updated to expect the new key; that shape change is intended. Co-Authored-By: Claude Opus 5 (1M context) --- src/py/mat3ra/notebooks_utils/core/entity/material/api.py | 8 +++++--- tests/py/unit/core/entity/test_material_api.py | 5 ++++- 2 files changed, 9 insertions(+), 4 deletions(-) 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 ab725991..777310ea 100644 --- a/src/py/mat3ra/notebooks_utils/core/entity/material/api.py +++ b/src/py/mat3ra/notebooks_utils/core/entity/material/api.py @@ -144,11 +144,13 @@ 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"] - # isEntitySet belongs in the query for the same reason owner does: the server truncates, so a - # response-side filter can drop real members along with the set document. - members = api_client.materials.list( + # isEntitySet belongs in the query for the same reason owner does -- the server truncates, so a + # response-side filter can drop real members along with the set document. The local filter stays + # as a guard: this platform has been observed ignoring list queries. + 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/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}, } From c0032d23baf59ee6855a03db7ec76416b4141aef Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 25 Aug 2026 00:07:52 -0700 Subject: [PATCH 07/20] Fix: keep the owner guard in the bulk resolver [SOF-8034] list_materials_in_set narrows the query and keeps its local filter, on the stated premise that this platform has been observed ignoring list queries. The bulk resolver dropped its guard on the opposite premise, in the same commit. If the query is ignored, next(iter(matches)) returns another account's hash twin -- the incident this resolver exists to prevent -- and the "not present for this account" error becomes unreachable. Make the two agree. Co-Authored-By: Claude Opus 5 (1M context) --- src/py/mat3ra/notebooks_utils/core/entity/material/api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 777310ea..a79c9cbb 100644 --- a/src/py/mat3ra/notebooks_utils/core/entity/material/api.py +++ b/src/py/mat3ra/notebooks_utils/core/entity/material/api.py @@ -73,7 +73,10 @@ def _require_material_for_owner(api_client: APIClient, query: dict, owner_id: st # Owner belongs in the query, not in a filter over the response: the server truncates, and the # account's own material is routinely absent from a page full of other owners' hash twins. matches = api_client.materials.list({**query, "owner._id": owner_id}) - material_response = next(iter(matches), None) + # Keep the owner check as a guard, matching list_materials_in_set: if the query is ignored, the + # first match is some other account's hash twin -- the exact incident this resolver exists to + # prevent -- and the "not present for this account" error below would never be raised. + material_response = next((item for item in matches if item.get("owner", {}).get("_id") == owner_id), None) if material_response is None: raise ValueError( "The bulk material resolved from metadata is not present on the platform for this account. " From bb173421c8dc8f6c1605f3ad5b4ff02415910b2c Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 25 Aug 2026 09:05:08 -0700 Subject: [PATCH 08/20] Fix: list mat3ra-code and mat3ra-esse with the made GH wheel [SOF-8034] Installing made from a wheel URL does not pull its deps, which is what the note in this file already said. Without them made fails to resolve in pyodide. Co-Authored-By: Claude Opus 5 (1M context) --- config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config.yml b/config.yml index 784dec57..19fd38f3 100644 --- a/config.yml +++ b/config.yml @@ -30,11 +30,11 @@ notebooks: - pymatgen-analysis-defects<=2024.4.23 - mat3ra-periodic-table # TEMPORARY [SOF-8034]: WIP wheel from made#297. Revert to `- mat3ra-made` on release. + # Installing made from a GH wheel means its deps are not resolved for us -- mat3ra-code and + # mat3ra-esse have to be listed explicitly, per the note this replaces. - https://mat3ra.github.io/made/mat3ra_made-0.1.dev1+g536f0ff28-py3-none-any.whl - # packages below are used when made is installed from GH wheel (made should be installed from below) - # - https://exabyte-io.github.io/made/mat3ra_made-0.1.dev1+ge64d360b8-py3-none-any.whl - # - mat3ra-code - # - mat3ra-esse + - mat3ra-code + - mat3ra-esse - name: import_material_from_jarvis_db_entry.ipynb packages_pyodide: - express-py==2024.2.2.post2 From e4f6415dfb1f698b45f14d5274c36ae7b6b7803c Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 25 Aug 2026 09:24:17 -0700 Subject: [PATCH 09/20] update: cleanup + remove commetns --- config.yml | 1 - pyproject.toml | 2 -- src/py/mat3ra/notebooks_utils/core/entity/material/api.py | 8 +------- tests/py/unit/core/entity/test_material_analysis.py | 2 -- 4 files changed, 1 insertion(+), 12 deletions(-) diff --git a/config.yml b/config.yml index 19fd38f3..d70d052b 100644 --- a/config.yml +++ b/config.yml @@ -29,7 +29,6 @@ notebooks: - emfs:/drive/packages/pydantic-2.7.1-py3-none-any.whl - pymatgen-analysis-defects<=2024.4.23 - mat3ra-periodic-table - # TEMPORARY [SOF-8034]: WIP wheel from made#297. Revert to `- mat3ra-made` on release. # Installing made from a GH wheel means its deps are not resolved for us -- mat3ra-code and # mat3ra-esse have to be listed explicitly, per the note this replaces. - https://mat3ra.github.io/made/mat3ra_made-0.1.dev1+g536f0ff28-py3-none-any.whl diff --git a/pyproject.toml b/pyproject.toml index 2ad655f4..b43db089 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,8 +36,6 @@ materials = [ "mat3ra-notebooks-utils[utils_standata]", "pymatgen==2024.4.13", "pymatgen-analysis-defects<=2024.4.23", - # TEMPORARY [SOF-8034]: WIP wheel built from made#297 by build-wheel.yml. Revert to the - # bare "mat3ra-made" once made#297 merges and publishes to PyPI. "mat3ra-made @ https://mat3ra.github.io/made/mat3ra_made-0.1.dev1+g536f0ff28-py3-none-any.whl", "mat3ra-periodic-table" ] 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 a79c9cbb..71899669 100644 --- a/src/py/mat3ra/notebooks_utils/core/entity/material/api.py +++ b/src/py/mat3ra/notebooks_utils/core/entity/material/api.py @@ -73,10 +73,7 @@ def _require_material_for_owner(api_client: APIClient, query: dict, owner_id: st # Owner belongs in the query, not in a filter over the response: the server truncates, and the # account's own material is routinely absent from a page full of other owners' hash twins. matches = api_client.materials.list({**query, "owner._id": owner_id}) - # Keep the owner check as a guard, matching list_materials_in_set: if the query is ignored, the - # first match is some other account's hash twin -- the exact incident this resolver exists to - # prevent -- and the "not present for this account" error below would never be raised. - material_response = next((item for item in matches if item.get("owner", {}).get("_id") == owner_id), None) + 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. " @@ -147,9 +144,6 @@ 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"] - # isEntitySet belongs in the query for the same reason owner does -- the server truncates, so a - # response-side filter can drop real members along with the set document. The local filter stays - # as a guard: this platform has been observed ignoring list queries. matches = api_client.materials.list( {"owner._id": owner_id, "inSet._id": material_set_id, "isEntitySet": {"$ne": True}} ) diff --git a/tests/py/unit/core/entity/test_material_analysis.py b/tests/py/unit/core/entity/test_material_analysis.py index 00d0c228..7e3dc52b 100644 --- a/tests/py/unit/core/entity/test_material_analysis.py +++ b/tests/py/unit/core/entity/test_material_analysis.py @@ -41,8 +41,6 @@ def test_slab_bulk_crystal_is_the_material_the_slab_was_built_from(): conventional = CrystalLatticePlanesMaterialAnalyzer( material=primitive, miller_indices=(0, 0, 1) ).material_with_conventional_lattice - # Without this the test would pass against broken code if the standata entry ever became the - # conventional cell -- the two hashes have to differ for the assertion below to mean anything. assert conventional.hash != primitive.hash slab = create_slab(crystal=primitive, miller_indices=(0, 0, 1), number_of_layers=3) From ca1688c968f140ce4a4cb1ff3b2bfaf8bc691934 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 25 Aug 2026 09:27:08 -0700 Subject: [PATCH 10/20] update: cleanup + remove commetns --- src/py/mat3ra/notebooks_utils/core/entity/material/api.py | 2 -- 1 file changed, 2 deletions(-) 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 71899669..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,6 @@ 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: - # Owner belongs in the query, not in a filter over the response: the server truncates, and the - # account's own material is routinely absent from a page full of other owners' hash twins. matches = api_client.materials.list({**query, "owner._id": owner_id}) material_response = next(iter(matches), None) if material_response is None: From 1ace7c4c3c43dc59af1f71670d1be08466b5ade2 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 25 Aug 2026 12:08:03 -0700 Subject: [PATCH 11/20] Fix: default the bulk total-energy source to my_account [SOF-8034] Surface and interfacial energy subtract this energy from a slab or interface SCF the caller is about to run, so it is only valid if both share the cutoff, k-grid and pseudopotentials. 'group' pins the code and functional, not the convergence, so 'public' can return a well-formed number that is invalid to subtract. Measured for Cu on the platform: nine total_energy properties share exabyteId EhsnH8uH7q3dYtLPM, and 'public' sorted by precision returns -175800.328 (qe:dft:gga:pbe, precision 2000) ahead of the correct -5493.740 -- same group, same precision, so neither the group filter nor the precision sort separates them. The account's own -5493.677 is unrefined and carries precision 216, so precision-descending ranks it last and 'public' can never select it. Every notebook already passes source=BULK_TOTAL_ENERGY_SOURCE='my_account'; this closes the gap for any caller that omits the argument, which now fails closed with a clear not-found instead of silently borrowing another account's energy. 39 passed. Co-Authored-By: Claude Opus 5 (1M context) --- src/py/mat3ra/notebooks_utils/core/entity/property/api.py | 8 ++++++-- .../test_property_api_find_total_energy_for_material.py | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) 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..ed5e3cb2 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,11 @@ 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`. Defaults to `my_account` because the caller subtracts this energy from its own + slab or interface SCF, and only an energy it computed itself is guaranteed to share the + cutoff, k-grid and pseudopotentials. `group` pins the code and functional, not the + convergence, so `public` can return a well-formed number that is invalid to subtract. Returns: The best-precision total_energy property, or None if none exists. 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}, ) From bffde8a236a8e5bfe927efe4147365ab4178f93b Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 25 Aug 2026 12:32:17 -0700 Subject: [PATCH 12/20] Chore: consume the standata WIP wheel [SOF-8034] standata publishes to PyPI only from main, so the scoped E_BULK resolution could not be exercised before release. Pin the pre-release wheel from standata feature/SOF-8034 in both pyproject.toml and config.yml, the latter being what JupyterLite micropips. TEMPORARY, alongside the made pin. Co-Authored-By: Claude Opus 5 (1M context) --- config.yml | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config.yml b/config.yml index d70d052b..73a7f097 100644 --- a/config.yml +++ b/config.yml @@ -51,7 +51,7 @@ notebooks: - emfs:/drive/packages/watchdog-2.3.1-py3-none-any.whl - name: specific_examples packages_pyodide: - - mat3ra-standata + - https://github.com/mat3ra/standata/releases/download/wip-95d5034/mat3ra_standata-2026.8.18.post1.dev2%2Bg95d50348-py3-none-any.whl # - mat3ra-notebooks-utils[utils_standata] - name: analyze_convex_hull packages_pyodide: @@ -77,7 +77,7 @@ notebooks: - jinja2 - emfs:/drive/packages/pydantic_core-2.18.2-py3-none-any.whl - emfs:/drive/packages/pydantic-2.7.1-py3-none-any.whl - - mat3ra-standata + - https://github.com/mat3ra/standata/releases/download/wip-95d5034/mat3ra_standata-2026.8.18.post1.dev2%2Bg95d50348-py3-none-any.whl - mat3ra-ide - mat3ra-api-client - requests diff --git a/pyproject.toml b/pyproject.toml index b43db089..9243692e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ jupyterlite = [ ] utils_standata = [ "mat3ra-utils", - "mat3ra-standata", + "mat3ra-standata @ https://github.com/mat3ra/standata/releases/download/wip-95d5034/mat3ra_standata-2026.8.18.post1.dev2%2Bg95d50348-py3-none-any.whl", ] api = [ "mat3ra-api-client", From 4e22c70e188ac405530118410845bc589455210b Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 25 Aug 2026 13:07:30 -0700 Subject: [PATCH 13/20] Chore: trim the source docstring [SOF-8034] Comment-only. Co-Authored-By: Claude Opus 5 (1M context) --- src/py/mat3ra/notebooks_utils/core/entity/property/api.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 ed5e3cb2..050c16f4 100644 --- a/src/py/mat3ra/notebooks_utils/core/entity/property/api.py +++ b/src/py/mat3ra/notebooks_utils/core/entity/property/api.py @@ -85,10 +85,7 @@ def find_total_energy_for_material(client: APIClient, material_id: str, source: 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: `my_account` (default), `curators` or - `public`. Defaults to `my_account` because the caller subtracts this energy from its own - slab or interface SCF, and only an energy it computed itself is guaranteed to share the - cutoff, k-grid and pseudopotentials. `group` pins the code and functional, not the - convergence, so `public` can return a well-formed number that is invalid to subtract. + `public`. Returns: The best-precision total_energy property, or None if none exists. From 45bb2feded26ccac53113eb6c5bdeb4dd587e9ae Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 25 Aug 2026 13:35:22 -0700 Subject: [PATCH 14/20] Fix: use a literal + in the standata wheel URL, not %2B [SOF-8034] micropip derives the version from the URL filename without decoding it, so a percent-encoded plus reaches packaging.Version() intact and the install dies: InvalidVersion: Invalid version: '2026.8.18.post1.dev2%2Bg95d50348' A + is legal unencoded in a URL path segment, and the made pin has always used one -- which is why that wheel installed and this one did not. Co-Authored-By: Claude Opus 5 (1M context) --- config.yml | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config.yml b/config.yml index 73a7f097..505e97b9 100644 --- a/config.yml +++ b/config.yml @@ -51,7 +51,7 @@ notebooks: - emfs:/drive/packages/watchdog-2.3.1-py3-none-any.whl - name: specific_examples packages_pyodide: - - https://github.com/mat3ra/standata/releases/download/wip-95d5034/mat3ra_standata-2026.8.18.post1.dev2%2Bg95d50348-py3-none-any.whl + - https://github.com/mat3ra/standata/releases/download/wip-95d5034/mat3ra_standata-2026.8.18.post1.dev2+g95d50348-py3-none-any.whl # - mat3ra-notebooks-utils[utils_standata] - name: analyze_convex_hull packages_pyodide: @@ -77,7 +77,7 @@ notebooks: - jinja2 - emfs:/drive/packages/pydantic_core-2.18.2-py3-none-any.whl - emfs:/drive/packages/pydantic-2.7.1-py3-none-any.whl - - https://github.com/mat3ra/standata/releases/download/wip-95d5034/mat3ra_standata-2026.8.18.post1.dev2%2Bg95d50348-py3-none-any.whl + - https://github.com/mat3ra/standata/releases/download/wip-95d5034/mat3ra_standata-2026.8.18.post1.dev2+g95d50348-py3-none-any.whl - mat3ra-ide - mat3ra-api-client - requests diff --git a/pyproject.toml b/pyproject.toml index 9243692e..6530dbc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ jupyterlite = [ ] utils_standata = [ "mat3ra-utils", - "mat3ra-standata @ https://github.com/mat3ra/standata/releases/download/wip-95d5034/mat3ra_standata-2026.8.18.post1.dev2%2Bg95d50348-py3-none-any.whl", + "mat3ra-standata @ https://github.com/mat3ra/standata/releases/download/wip-95d5034/mat3ra_standata-2026.8.18.post1.dev2+g95d50348-py3-none-any.whl", ] api = [ "mat3ra-api-client", From 71ae5457e751683c5002604616673c1ca3ccec95 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 25 Aug 2026 13:57:41 -0700 Subject: [PATCH 15/20] Fix: serve the standata wheel same-origin for JupyterLite [SOF-8034] GitHub release assets 302 to objects.githubusercontent.com with no access-control-allow-origin, so micropip's browser fetch is blocked and install_packages dies with 'OSError: Failed to fetch' -- taking every later cell with it as NameError. curl hides this because it ignores CORS. Use the emfs:/drive/packages/ form the other pinned wheels already use, which is served from the JupyterLite origin. pyproject.toml keeps the URL: pip is not subject to CORS. Co-Authored-By: Claude Opus 5 (1M context) --- config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.yml b/config.yml index 505e97b9..7983610c 100644 --- a/config.yml +++ b/config.yml @@ -51,7 +51,7 @@ notebooks: - emfs:/drive/packages/watchdog-2.3.1-py3-none-any.whl - name: specific_examples packages_pyodide: - - https://github.com/mat3ra/standata/releases/download/wip-95d5034/mat3ra_standata-2026.8.18.post1.dev2+g95d50348-py3-none-any.whl + - emfs:/drive/packages/mat3ra_standata-2026.8.18.post1.dev2+g95d50348-py3-none-any.whl # - mat3ra-notebooks-utils[utils_standata] - name: analyze_convex_hull packages_pyodide: @@ -77,7 +77,7 @@ notebooks: - jinja2 - emfs:/drive/packages/pydantic_core-2.18.2-py3-none-any.whl - emfs:/drive/packages/pydantic-2.7.1-py3-none-any.whl - - https://github.com/mat3ra/standata/releases/download/wip-95d5034/mat3ra_standata-2026.8.18.post1.dev2+g95d50348-py3-none-any.whl + - emfs:/drive/packages/mat3ra_standata-2026.8.18.post1.dev2+g95d50348-py3-none-any.whl - mat3ra-ide - mat3ra-api-client - requests From 0b38e7825cd54553d20bf47975c654b237949370 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 25 Aug 2026 14:09:27 -0700 Subject: [PATCH 16/20] Fix: list standata's deps alongside its wheel [SOF-8034] A wheel-URL install does not resolve dependencies, so mat3ra/standata/base.py died on 'import pandas'. Same class as made needing mat3ra-code and mat3ra-esse. Co-Authored-By: Claude Opus 5 (1M context) --- config.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config.yml b/config.yml index 7983610c..0e0bb2dc 100644 --- a/config.yml +++ b/config.yml @@ -52,6 +52,9 @@ notebooks: - name: specific_examples packages_pyodide: - emfs:/drive/packages/mat3ra_standata-2026.8.18.post1.dev2+g95d50348-py3-none-any.whl + - pyyaml + - pandas + - typer # - mat3ra-notebooks-utils[utils_standata] - name: analyze_convex_hull packages_pyodide: @@ -78,6 +81,9 @@ notebooks: - emfs:/drive/packages/pydantic_core-2.18.2-py3-none-any.whl - emfs:/drive/packages/pydantic-2.7.1-py3-none-any.whl - emfs:/drive/packages/mat3ra_standata-2026.8.18.post1.dev2+g95d50348-py3-none-any.whl + - pyyaml + - pandas + - typer - mat3ra-ide - mat3ra-api-client - requests From 8cc6ca4bf06e6fe94a8398e56f445547fa743715 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 25 Aug 2026 14:11:08 -0700 Subject: [PATCH 17/20] Fix: resolve a bulk crystal by _id, then hash, then scaledHash [SOF-8034] The order was reversed: scaledHash first, widest to narrowest. scaledHash normalises the lattice constant away, so it matches structurally different materials -- 83 platform materials share Cu mp-30's scaledHash across 9 distinct exabyteIds, while all 51 sharing its hash carry one. Since E_bulk depends on the lattice constant, a scaledHash match is the wrong reference for an energy. Same reversal applied to BULK_QUERY in standata's resolve_bulk_from_slab_by_build. 39 passed. Co-Authored-By: Claude Opus 5 (1M context) --- .../notebooks_utils/core/entity/material/analysis.py | 2 +- tests/py/unit/core/entity/test_material_analysis.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) 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/tests/py/unit/core/entity/test_material_analysis.py b/tests/py/unit/core/entity/test_material_analysis.py index 7e3dc52b..ac99995f 100644 --- a/tests/py/unit/core/entity/test_material_analysis.py +++ b/tests/py/unit/core/entity/test_material_analysis.py @@ -15,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 From c7b86baf3d894671daa8cd1bc45ca004092aa8e9 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 25 Aug 2026 14:14:14 -0700 Subject: [PATCH 18/20] Chore: use the standata wheel with all three workflows scoped [SOF-8034] Rebuilt from standata 4f3e917f, which scopes E_BULK for surface, interfacial and defect-formation energy. Served from the JupyterLite origin via emfs: -- GitHub release assets send no access-control-allow-origin, so micropip cannot fetch them. pyproject.toml goes back to a bare mat3ra-standata: a direct URL there would be rejected on PyPI upload, and the unit tests do not exercise standata workflows. Co-Authored-By: Claude Opus 5 (1M context) --- config.yml | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config.yml b/config.yml index 0e0bb2dc..6afe773e 100644 --- a/config.yml +++ b/config.yml @@ -51,7 +51,7 @@ notebooks: - emfs:/drive/packages/watchdog-2.3.1-py3-none-any.whl - name: specific_examples packages_pyodide: - - emfs:/drive/packages/mat3ra_standata-2026.8.18.post1.dev2+g95d50348-py3-none-any.whl + - emfs:/drive/packages/mat3ra_standata-2026.8.18.post1.dev6+g4f3e917f-py3-none-any.whl - pyyaml - pandas - typer @@ -80,7 +80,7 @@ notebooks: - jinja2 - emfs:/drive/packages/pydantic_core-2.18.2-py3-none-any.whl - emfs:/drive/packages/pydantic-2.7.1-py3-none-any.whl - - emfs:/drive/packages/mat3ra_standata-2026.8.18.post1.dev2+g95d50348-py3-none-any.whl + - emfs:/drive/packages/mat3ra_standata-2026.8.18.post1.dev6+g4f3e917f-py3-none-any.whl - pyyaml - pandas - typer diff --git a/pyproject.toml b/pyproject.toml index 6530dbc0..b43db089 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ jupyterlite = [ ] utils_standata = [ "mat3ra-utils", - "mat3ra-standata @ https://github.com/mat3ra/standata/releases/download/wip-95d5034/mat3ra_standata-2026.8.18.post1.dev2+g95d50348-py3-none-any.whl", + "mat3ra-standata", ] api = [ "mat3ra-api-client", From df833c0d0d9c87131edc14a79edceef07aa9344f Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 25 Aug 2026 18:22:53 -0700 Subject: [PATCH 19/20] Fix: get_for_job already unwraps data [SOF-8034] --- other/materials_designer/workflows/interfacial_energy.ipynb | 2 +- other/materials_designer/workflows/surface_energy.ipynb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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\")" ] From b66f6a36ceefe7154e55170bad8d80d992bf3c0f Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Tue, 25 Aug 2026 21:25:20 -0700 Subject: [PATCH 20/20] Chore: drop the temporary wheel and package pins [SOF-8034] made 2026.8.26.post0 is on PyPI with the input-crystal fix, so config.yml and pyproject.toml go back to a bare mat3ra-made. The standata wheel and the pyyaml/pandas/typer lines that a wheel-URL install needed go with it -- those are standata's own declared dependencies and a PyPI install resolves them. Co-Authored-By: Claude Opus 5 (1M context) --- config.yml | 20 +++++++------------- pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/config.yml b/config.yml index 6afe773e..5a5c5477 100644 --- a/config.yml +++ b/config.yml @@ -29,11 +29,11 @@ notebooks: - emfs:/drive/packages/pydantic-2.7.1-py3-none-any.whl - pymatgen-analysis-defects<=2024.4.23 - mat3ra-periodic-table - # Installing made from a GH wheel means its deps are not resolved for us -- mat3ra-code and - # mat3ra-esse have to be listed explicitly, per the note this replaces. - - https://mat3ra.github.io/made/mat3ra_made-0.1.dev1+g536f0ff28-py3-none-any.whl - - mat3ra-code - - mat3ra-esse + - mat3ra-made + # packages below are used when made is installed from GH wheel (made should be installed from below) + # - https://exabyte-io.github.io/made/mat3ra_made-0.1.dev1+ge64d360b8-py3-none-any.whl + # - mat3ra-code + # - mat3ra-esse - name: import_material_from_jarvis_db_entry.ipynb packages_pyodide: - express-py==2024.2.2.post2 @@ -51,10 +51,7 @@ notebooks: - emfs:/drive/packages/watchdog-2.3.1-py3-none-any.whl - name: specific_examples packages_pyodide: - - emfs:/drive/packages/mat3ra_standata-2026.8.18.post1.dev6+g4f3e917f-py3-none-any.whl - - pyyaml - - pandas - - typer + - mat3ra-standata # - mat3ra-notebooks-utils[utils_standata] - name: analyze_convex_hull packages_pyodide: @@ -80,10 +77,7 @@ notebooks: - jinja2 - emfs:/drive/packages/pydantic_core-2.18.2-py3-none-any.whl - emfs:/drive/packages/pydantic-2.7.1-py3-none-any.whl - - emfs:/drive/packages/mat3ra_standata-2026.8.18.post1.dev6+g4f3e917f-py3-none-any.whl - - pyyaml - - pandas - - typer + - mat3ra-standata - mat3ra-ide - mat3ra-api-client - requests diff --git a/pyproject.toml b/pyproject.toml index b43db089..bd6adf9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ materials = [ "mat3ra-notebooks-utils[utils_standata]", "pymatgen==2024.4.13", "pymatgen-analysis-defects<=2024.4.23", - "mat3ra-made @ https://mat3ra.github.io/made/mat3ra_made-0.1.dev1+g536f0ff28-py3-none-any.whl", + "mat3ra-made", "mat3ra-periodic-table" ] workflows = [