From 17439d16fa01995e65ca4557f941c2c9757ca610 Mon Sep 17 00:00:00 2001 From: Jan Janssen Date: Thu, 5 Oct 2023 16:14:32 +0200 Subject: [PATCH 01/17] Add mp-api interface --- .ci_support/environment.yml | 1 + .../build/materialsproject.py | 139 ++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 src/structuretoolkit/build/materialsproject.py diff --git a/.ci_support/environment.yml b/.ci_support/environment.yml index 5224c6701..4a659480c 100644 --- a/.ci_support/environment.yml +++ b/.ci_support/environment.yml @@ -20,3 +20,4 @@ dependencies: - sqsgenerator =0.5.4 - hatchling =1.29.0 - hatch-vcs =0.5.0 +- mp-api =0.37.2 diff --git a/src/structuretoolkit/build/materialsproject.py b/src/structuretoolkit/build/materialsproject.py new file mode 100644 index 000000000..094326f97 --- /dev/null +++ b/src/structuretoolkit/build/materialsproject.py @@ -0,0 +1,139 @@ +from typing import Union, List, Generator +from ase.atoms import Atoms +from structuretoolkit.common.pymatgen import pymatgen_to_ase + + +class MaterialsProject: + """ + Convenience interface to the Materials Project Structure Database. + + Usage is only possible with an API key obtained from the Materials Project. To do this, create an account with + them, login and access `this webpage `. + + Once you have a key, either pass it as the `api_key` parameter in the methods of this object or export an + environment variable, called `MP_API_KEY`, in your shell setup. + """ + + @staticmethod + def search( + chemsys: Union[str, List[str]], api_key=None, **kwargs + ) -> Generator[dict[str, ...]]: + """ + Search the database for all structures matching the given query. + + Note that `chemsys` takes distint values for unaries, binaries and so! A query with `chemsys=["Fe", "O"]` will + return iron structures and oxygen structures, but no iron oxide structures. Similarily `chemsys=["Fe-O"]` will + not return unary structures. + + All keyword arguments for filtering from the original API are supported. See the + `original docs `_ for them. + + Search for all iron structures: + + >>> pr = Project(...) + >>> irons = pr.create.structure.materialsproject.search("Fe") + >>> irons.number_of_structures + 10 + + The returned :class:`~.MPQueryResults` object implements :class:`~.HasStructure` and can be accessed with the + material ids as a short-hand + + >>> irons.get_structure(1) == irons.get_structure('mp-13') + True + + Search for all structures with Al, Li that are on the T=0 convex hull: + + >>> alli = pr.create.structure.materialsproject.search(['Al', 'Li', 'Al-Li'], is_stable=True) + >>> len(alli) + 6 + + Args: + chemsys (str, list of str): confine search to given elements; either an element symbol or multiple element + symbols seperated by dashes; if a list of strings is given return structures matching either of them + api_key (str, optional): if your API key is not exported in the environment flag MP_API_KEY, pass it here + **kwargs: passed verbatim to :meth:`mp_api.MPRester.summary.search` to further filter the results + + Returns: + :class:`~.MPQueryResults`: resulting structures from the query + """ + from mp_api.client import MPRester + + rest_kwargs = { + "use_document_model": False, # returns results as dictionaries + "include_user_agent": True, # send some additional software version info to MP + } + if api_key is not None: + rest_kwargs["api_key"] = api_key + with MPRester(**rest_kwargs) as mpr: + results = mpr.summary.search( + chemsys=chemsys, **kwargs, fields=["structure", "material_id"] + ) + for r in results: + if 'structure' in r: + r['structure'] = pymatgen_to_ase(r['structure']) + yield r + + @staticmethod + def by_id( + material_id: Union[str, int], + final: bool = True, + conventional_unit_cell: bool = False, + api_key=None, + ) -> Union[Atoms, List[Atoms]]: + """ + Retrieve a structure by material id. + + This is how you would ask for the iron ground state: + + >>> pr = Project(...) + >>> pr.create.structure.materialsproject.by_id('mp-13') + Fe: [0. 0. 0.] + tags: + spin: [(0: 2.214)] + pbc: [ True True True] + cell: + Cell([[2.318956, 0.000185, -0.819712], [-1.159251, 2.008215, -0.819524], [2.5e-05, 0.000273, 2.459206]]) + + + Args: + material_id (str): the id assigned to a structure by the materials project + api_key (str, optional): if your API key is not exported in the environment flag MP_API_KEY, pass it here + final (bool, optional): if set to False, returns the list of initial structures, + else returns the final structure. (Default is True) + conventional_unit_cell (bool, optional): if set to True, returns the standard conventional unit cell. + (Default is False) + + Returns: + :class:`~.Atoms`: requested final structure if final is True + list of :class:~.Atoms`: a list of initial (pre-relaxation) structures if final is False + + Raises: + ValueError: material id does not exist + """ + from mp_api.client import MPRester + + rest_kwargs = { + "include_user_agent": True, # send some additional software version info to MP + } + if api_key is not None: + rest_kwargs["api_key"] = api_key + with MPRester(**rest_kwargs) as mpr: + if final: + return pymatgen_to_ase( + mpr.get_structure_by_material_id( + material_id=material_id, + final=final, + conventional_unit_cell=conventional_unit_cell, + ) + ) + else: + return [ + pymatgen_to_ase(mpr_structure) + for mpr_structure in ( + mpr.get_structure_by_material_id( + material_id=material_id, + final=final, + conventional_unit_cell=conventional_unit_cell, + ) + ) + ] From 1dbfa9856e6169dcff5e1888e5101dd738ef5770 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Thu, 2 Apr 2026 17:25:57 -0400 Subject: [PATCH 02/17] Drop namespace class --- .../build/materialsproject.py | 238 +++++++++--------- 1 file changed, 118 insertions(+), 120 deletions(-) diff --git a/src/structuretoolkit/build/materialsproject.py b/src/structuretoolkit/build/materialsproject.py index 094326f97..701a6e855 100644 --- a/src/structuretoolkit/build/materialsproject.py +++ b/src/structuretoolkit/build/materialsproject.py @@ -1,139 +1,137 @@ -from typing import Union, List, Generator +from typing import Any, Generator from ase.atoms import Atoms from structuretoolkit.common.pymatgen import pymatgen_to_ase -class MaterialsProject: +def search( + chemsys: str | list[str], api_key=None, **kwargs +) -> Generator[dict[str, Any]]: """ - Convenience interface to the Materials Project Structure Database. + Search the database for all structures matching the given query. + + Note that `chemsys` takes distint values for unaries, binaries and so! A query with `chemsys=["Fe", "O"]` will + return iron structures and oxygen structures, but no iron oxide structures. Similarily `chemsys=["Fe-O"]` will + not return unary structures. + + All keyword arguments for filtering from the original API are supported. See the + `original docs `_ for them. + + Search for all iron structures: + + >>> pr = Project(...) + >>> irons = pr.create.structure.materialsproject.search("Fe") + >>> irons.number_of_structures + 10 + + The returned :class:`~.MPQueryResults` object implements :class:`~.HasStructure` and can be accessed with the + material ids as a short-hand + + >>> irons.get_structure(1) == irons.get_structure('mp-13') + True + + Search for all structures with Al, Li that are on the T=0 convex hull: + + >>> alli = pr.create.structure.materialsproject.search(['Al', 'Li', 'Al-Li'], is_stable=True) + >>> len(alli) + 6 Usage is only possible with an API key obtained from the Materials Project. To do this, create an account with them, login and access `this webpage `. - Once you have a key, either pass it as the `api_key` parameter in the methods of this object or export an + Once you have a key, either pass it as the `api_key` parameter or export an environment variable, called `MP_API_KEY`, in your shell setup. + + Args: + chemsys (str, list of str): confine search to given elements; either an element symbol or multiple element + symbols seperated by dashes; if a list of strings is given return structures matching either of them + api_key (str, optional): if your API key is not exported in the environment flag MP_API_KEY, pass it here + **kwargs: passed verbatim to :meth:`mp_api.MPRester.summary.search` to further filter the results + + Returns: + :class:`~.MPQueryResults`: resulting structures from the query + """ + from mp_api.client import MPRester + + rest_kwargs = { + "use_document_model": False, # returns results as dictionaries + "include_user_agent": True, # send some additional software version info to MP + } + if api_key is not None: + rest_kwargs["api_key"] = api_key + with MPRester(**rest_kwargs) as mpr: + results = mpr.summary.search( + chemsys=chemsys, **kwargs, fields=["structure", "material_id"] + ) + for r in results: + if 'structure' in r: + r['structure'] = pymatgen_to_ase(r['structure']) + yield r + +def by_id( + material_id: str | int, + final: bool = True, + conventional_unit_cell: bool = False, + api_key=None, +) -> Atoms | list[Atoms]: """ + Retrieve a structure by material id. + + This is how you would ask for the iron ground state: - @staticmethod - def search( - chemsys: Union[str, List[str]], api_key=None, **kwargs - ) -> Generator[dict[str, ...]]: - """ - Search the database for all structures matching the given query. - - Note that `chemsys` takes distint values for unaries, binaries and so! A query with `chemsys=["Fe", "O"]` will - return iron structures and oxygen structures, but no iron oxide structures. Similarily `chemsys=["Fe-O"]` will - not return unary structures. - - All keyword arguments for filtering from the original API are supported. See the - `original docs `_ for them. - - Search for all iron structures: - - >>> pr = Project(...) - >>> irons = pr.create.structure.materialsproject.search("Fe") - >>> irons.number_of_structures - 10 - - The returned :class:`~.MPQueryResults` object implements :class:`~.HasStructure` and can be accessed with the - material ids as a short-hand - - >>> irons.get_structure(1) == irons.get_structure('mp-13') - True - - Search for all structures with Al, Li that are on the T=0 convex hull: - - >>> alli = pr.create.structure.materialsproject.search(['Al', 'Li', 'Al-Li'], is_stable=True) - >>> len(alli) - 6 - - Args: - chemsys (str, list of str): confine search to given elements; either an element symbol or multiple element - symbols seperated by dashes; if a list of strings is given return structures matching either of them - api_key (str, optional): if your API key is not exported in the environment flag MP_API_KEY, pass it here - **kwargs: passed verbatim to :meth:`mp_api.MPRester.summary.search` to further filter the results - - Returns: - :class:`~.MPQueryResults`: resulting structures from the query - """ - from mp_api.client import MPRester - - rest_kwargs = { - "use_document_model": False, # returns results as dictionaries - "include_user_agent": True, # send some additional software version info to MP - } - if api_key is not None: - rest_kwargs["api_key"] = api_key - with MPRester(**rest_kwargs) as mpr: - results = mpr.summary.search( - chemsys=chemsys, **kwargs, fields=["structure", "material_id"] + >>> pr = Project(...) + >>> pr.create.structure.materialsproject.by_id('mp-13') + Fe: [0. 0. 0.] + tags: + spin: [(0: 2.214)] + pbc: [ True True True] + cell: + Cell([[2.318956, 0.000185, -0.819712], [-1.159251, 2.008215, -0.819524], [2.5e-05, 0.000273, 2.459206]]) + + Usage is only possible with an API key obtained from the Materials Project. To do this, create an account with + them, login and access `this webpage `. + + Once you have a key, either pass it as the `api_key` parameter or export an + environment variable, called `MP_API_KEY`, in your shell setup. + + Args: + material_id (str): the id assigned to a structure by the materials project + api_key (str, optional): if your API key is not exported in the environment flag MP_API_KEY, pass it here + final (bool, optional): if set to False, returns the list of initial structures, + else returns the final structure. (Default is True) + conventional_unit_cell (bool, optional): if set to True, returns the standard conventional unit cell. + (Default is False) + + Returns: + :class:`~.Atoms`: requested final structure if final is True + list of :class:~.Atoms`: a list of initial (pre-relaxation) structures if final is False + + Raises: + ValueError: material id does not exist + """ + from mp_api.client import MPRester + + rest_kwargs = { + "include_user_agent": True, # send some additional software version info to MP + } + if api_key is not None: + rest_kwargs["api_key"] = api_key + with MPRester(**rest_kwargs) as mpr: + if final: + return pymatgen_to_ase( + mpr.get_structure_by_material_id( + material_id=material_id, + final=final, + conventional_unit_cell=conventional_unit_cell, + ) ) - for r in results: - if 'structure' in r: - r['structure'] = pymatgen_to_ase(r['structure']) - yield r - - @staticmethod - def by_id( - material_id: Union[str, int], - final: bool = True, - conventional_unit_cell: bool = False, - api_key=None, - ) -> Union[Atoms, List[Atoms]]: - """ - Retrieve a structure by material id. - - This is how you would ask for the iron ground state: - - >>> pr = Project(...) - >>> pr.create.structure.materialsproject.by_id('mp-13') - Fe: [0. 0. 0.] - tags: - spin: [(0: 2.214)] - pbc: [ True True True] - cell: - Cell([[2.318956, 0.000185, -0.819712], [-1.159251, 2.008215, -0.819524], [2.5e-05, 0.000273, 2.459206]]) - - - Args: - material_id (str): the id assigned to a structure by the materials project - api_key (str, optional): if your API key is not exported in the environment flag MP_API_KEY, pass it here - final (bool, optional): if set to False, returns the list of initial structures, - else returns the final structure. (Default is True) - conventional_unit_cell (bool, optional): if set to True, returns the standard conventional unit cell. - (Default is False) - - Returns: - :class:`~.Atoms`: requested final structure if final is True - list of :class:~.Atoms`: a list of initial (pre-relaxation) structures if final is False - - Raises: - ValueError: material id does not exist - """ - from mp_api.client import MPRester - - rest_kwargs = { - "include_user_agent": True, # send some additional software version info to MP - } - if api_key is not None: - rest_kwargs["api_key"] = api_key - with MPRester(**rest_kwargs) as mpr: - if final: - return pymatgen_to_ase( + else: + return [ + pymatgen_to_ase(mpr_structure) + for mpr_structure in ( mpr.get_structure_by_material_id( material_id=material_id, final=final, conventional_unit_cell=conventional_unit_cell, ) ) - else: - return [ - pymatgen_to_ase(mpr_structure) - for mpr_structure in ( - mpr.get_structure_by_material_id( - material_id=material_id, - final=final, - conventional_unit_cell=conventional_unit_cell, - ) - ) - ] + ] From 98d0a2142c0096426f65073a223a9980e4a85664 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Thu, 2 Apr 2026 17:27:51 -0400 Subject: [PATCH 03/17] Add function imports to init --- src/structuretoolkit/build/__init__.py | 6 ++++++ src/structuretoolkit/build/materialsproject.py | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/structuretoolkit/build/__init__.py b/src/structuretoolkit/build/__init__.py index ab11d72b8..180c9df42 100644 --- a/src/structuretoolkit/build/__init__.py +++ b/src/structuretoolkit/build/__init__.py @@ -2,6 +2,10 @@ from structuretoolkit.build.compound import B2, C14, C15, C36, D03 from structuretoolkit.build.mesh import create_mesh from structuretoolkit.build.sqs import sqs_structures +from structuretoolkit.build.materialsproject import ( + search as materialsproject_search, + by_id as materialsproject_by_id, +) from structuretoolkit.build.surface import ( get_high_index_surface_info, high_index_surface, @@ -19,4 +23,6 @@ "sqs_structures", "get_high_index_surface_info", "high_index_surface", + "materialsproject_search", + "materialsproject_by_id", ] diff --git a/src/structuretoolkit/build/materialsproject.py b/src/structuretoolkit/build/materialsproject.py index 701a6e855..52e72cbb0 100644 --- a/src/structuretoolkit/build/materialsproject.py +++ b/src/structuretoolkit/build/materialsproject.py @@ -63,10 +63,11 @@ def search( chemsys=chemsys, **kwargs, fields=["structure", "material_id"] ) for r in results: - if 'structure' in r: - r['structure'] = pymatgen_to_ase(r['structure']) + if "structure" in r: + r["structure"] = pymatgen_to_ase(r["structure"]) yield r + def by_id( material_id: str | int, final: bool = True, From bb53de1a6fa121ae0b3ae1794b73d13142b6f484 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Thu, 2 Apr 2026 17:31:05 -0400 Subject: [PATCH 04/17] Fix typing import --- src/structuretoolkit/build/materialsproject.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structuretoolkit/build/materialsproject.py b/src/structuretoolkit/build/materialsproject.py index 52e72cbb0..b21943fb0 100644 --- a/src/structuretoolkit/build/materialsproject.py +++ b/src/structuretoolkit/build/materialsproject.py @@ -1,4 +1,5 @@ -from typing import Any, Generator +from typing import Any +from collections.abc import Generator from ase.atoms import Atoms from structuretoolkit.common.pymatgen import pymatgen_to_ase From 06431e54e32d8684866b07a29700650775ae1c7e Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Thu, 2 Apr 2026 18:44:37 -0400 Subject: [PATCH 05/17] Fix deps, typehints, comments add `fields` argument --- pyproject.toml | 1 + .../build/materialsproject.py | 35 +++++++++---------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6c9b246a7..1280eff0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ phonopy = [ "phonopy==3.3.0", "spglib==2.7.0", ] +mp-api = ["mp-api==0.37.2"] [tool.ruff] exclude = [".ci_support", "tests", "setup.py", "_version.py"] diff --git a/src/structuretoolkit/build/materialsproject.py b/src/structuretoolkit/build/materialsproject.py index b21943fb0..83c6684dd 100644 --- a/src/structuretoolkit/build/materialsproject.py +++ b/src/structuretoolkit/build/materialsproject.py @@ -1,12 +1,12 @@ -from typing import Any +from typing import Any, Iterable from collections.abc import Generator from ase.atoms import Atoms from structuretoolkit.common.pymatgen import pymatgen_to_ase def search( - chemsys: str | list[str], api_key=None, **kwargs -) -> Generator[dict[str, Any]]: + chemsys: str | list[str], fields: Iterable[str] = (), api_key=None, **kwargs +) -> Generator[dict[str, Any], None, None]: """ Search the database for all structures matching the given query. @@ -19,20 +19,13 @@ def search( Search for all iron structures: - >>> pr = Project(...) - >>> irons = pr.create.structure.materialsproject.search("Fe") - >>> irons.number_of_structures + >>> irons = structuretoolkit.build.materialsproject.search("Fe") + >>> len(irons) 10 - The returned :class:`~.MPQueryResults` object implements :class:`~.HasStructure` and can be accessed with the - material ids as a short-hand - - >>> irons.get_structure(1) == irons.get_structure('mp-13') - True - Search for all structures with Al, Li that are on the T=0 convex hull: - >>> alli = pr.create.structure.materialsproject.search(['Al', 'Li', 'Al-Li'], is_stable=True) + >>> alli = structuretoolkit.build.materialsproject.search(['Al', 'Li', 'Al-Li'], is_stable=True) >>> len(alli) 6 @@ -44,12 +37,17 @@ def search( Args: chemsys (str, list of str): confine search to given elements; either an element symbol or multiple element - symbols seperated by dashes; if a list of strings is given return structures matching either of them + symbols seperated by dashes; if a list of strings is given return structures matching either of them + fields (iterable of str): pass as `fields` to :meth:`mp_api.MPRester.summary.search` to request additional + database entries beyond the structure api_key (str, optional): if your API key is not exported in the environment flag MP_API_KEY, pass it here **kwargs: passed verbatim to :meth:`mp_api.MPRester.summary.search` to further filter the results Returns: - :class:`~.MPQueryResults`: resulting structures from the query + list of dict: one dictionary for each search results with at least keys + 'material_id': database key of the hit + 'structure': ASE atoms object + plus any requested via `fields`. """ from mp_api.client import MPRester @@ -61,12 +59,12 @@ def search( rest_kwargs["api_key"] = api_key with MPRester(**rest_kwargs) as mpr: results = mpr.summary.search( - chemsys=chemsys, **kwargs, fields=["structure", "material_id"] + chemsys=chemsys, **kwargs, fields=list(fields) + ["structure", "material_id"] ) for r in results: if "structure" in r: r["structure"] = pymatgen_to_ase(r["structure"]) - yield r + yield r def by_id( @@ -80,8 +78,7 @@ def by_id( This is how you would ask for the iron ground state: - >>> pr = Project(...) - >>> pr.create.structure.materialsproject.by_id('mp-13') + >>> structuretoolkit.build.materialsproject.by_id('mp-13') Fe: [0. 0. 0.] tags: spin: [(0: 2.214)] From 588be25c04f77c5202ae11e92aa2942b109f9411 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:53:30 +0000 Subject: [PATCH 06/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/structuretoolkit/build/materialsproject.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/structuretoolkit/build/materialsproject.py b/src/structuretoolkit/build/materialsproject.py index 83c6684dd..8badcda30 100644 --- a/src/structuretoolkit/build/materialsproject.py +++ b/src/structuretoolkit/build/materialsproject.py @@ -5,7 +5,7 @@ def search( - chemsys: str | list[str], fields: Iterable[str] = (), api_key=None, **kwargs + chemsys: str | list[str], fields: Iterable[str] = (), api_key=None, **kwargs ) -> Generator[dict[str, Any], None, None]: """ Search the database for all structures matching the given query. @@ -59,7 +59,9 @@ def search( rest_kwargs["api_key"] = api_key with MPRester(**rest_kwargs) as mpr: results = mpr.summary.search( - chemsys=chemsys, **kwargs, fields=list(fields) + ["structure", "material_id"] + chemsys=chemsys, + **kwargs, + fields=list(fields) + ["structure", "material_id"], ) for r in results: if "structure" in r: From d1755571059e1ee3a97fe8f9179df26b8deea5e2 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Thu, 2 Apr 2026 19:50:50 -0400 Subject: [PATCH 07/17] fix len --- src/structuretoolkit/build/materialsproject.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structuretoolkit/build/materialsproject.py b/src/structuretoolkit/build/materialsproject.py index 8badcda30..2f821530f 100644 --- a/src/structuretoolkit/build/materialsproject.py +++ b/src/structuretoolkit/build/materialsproject.py @@ -20,13 +20,13 @@ def search( Search for all iron structures: >>> irons = structuretoolkit.build.materialsproject.search("Fe") - >>> len(irons) + >>> len(list(irons)) 10 Search for all structures with Al, Li that are on the T=0 convex hull: >>> alli = structuretoolkit.build.materialsproject.search(['Al', 'Li', 'Al-Li'], is_stable=True) - >>> len(alli) + >>> len(list(alli)) 6 Usage is only possible with an API key obtained from the Materials Project. To do this, create an account with From 6cc4663e28fab4ecc34e3bfeaea5c90e7753b790 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:54:12 +0000 Subject: [PATCH 08/17] fix: apply CodeRabbit auto-fixes Fixed 2 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit --- src/structuretoolkit/_version.py | 32 +++++++++++-------- .../build/materialsproject.py | 8 ++--- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/structuretoolkit/_version.py b/src/structuretoolkit/_version.py index 1a870a8a4..388726f1b 100644 --- a/src/structuretoolkit/_version.py +++ b/src/structuretoolkit/_version.py @@ -1,20 +1,24 @@ -# file generated by setuptools-scm +# file generated by vcs-versioning # don't change, don't track in version control +from __future__ import annotations -__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] - -TYPE_CHECKING = False -if TYPE_CHECKING: - from typing import Tuple, Union - - VERSION_TUPLE = Tuple[Union[int, str], ...] -else: - VERSION_TUPLE = object +__all__ = [ + "__version__", + "__version_tuple__", + "version", + "version_tuple", + "__commit_id__", + "commit_id", +] version: str __version__: str -__version_tuple__: VERSION_TUPLE -version_tuple: VERSION_TUPLE +__version_tuple__: tuple[int | str, ...] +version_tuple: tuple[int | str, ...] +commit_id: str | None +__commit_id__: str | None + +__version__ = version = '0.1.dev1+gd17555710.d20260403' +__version_tuple__ = version_tuple = (0, 1, 'dev1', 'gd17555710.d20260403') -__version__ = version = "0.0.1" -__version_tuple__ = version_tuple = (0, 0, 1) +__commit_id__ = commit_id = None \ No newline at end of file diff --git a/src/structuretoolkit/build/materialsproject.py b/src/structuretoolkit/build/materialsproject.py index 2f821530f..c1fbba5f6 100644 --- a/src/structuretoolkit/build/materialsproject.py +++ b/src/structuretoolkit/build/materialsproject.py @@ -10,8 +10,8 @@ def search( """ Search the database for all structures matching the given query. - Note that `chemsys` takes distint values for unaries, binaries and so! A query with `chemsys=["Fe", "O"]` will - return iron structures and oxygen structures, but no iron oxide structures. Similarily `chemsys=["Fe-O"]` will + Note that `chemsys` takes distinct values for unaries, binaries and so! A query with `chemsys=["Fe", "O"]` will + return iron and oxygen structures but not iron oxide. Similarly `chemsys=["Fe-O"]` will not return unary structures. All keyword arguments for filtering from the original API are supported. See the @@ -37,7 +37,7 @@ def search( Args: chemsys (str, list of str): confine search to given elements; either an element symbol or multiple element - symbols seperated by dashes; if a list of strings is given return structures matching either of them + symbols separated by dashes; if a list of strings is given return structures matching either of them fields (iterable of str): pass as `fields` to :meth:`mp_api.MPRester.summary.search` to request additional database entries beyond the structure api_key (str, optional): if your API key is not exported in the environment flag MP_API_KEY, pass it here @@ -135,4 +135,4 @@ def by_id( conventional_unit_cell=conventional_unit_cell, ) ) - ] + ] \ No newline at end of file From b8a6f41c814d936452b3fd5e5b3e1e4f4085506f Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Fri, 3 Apr 2026 15:54:47 +0000 Subject: [PATCH 09/17] Format black --- src/structuretoolkit/_version.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/structuretoolkit/_version.py b/src/structuretoolkit/_version.py index 388726f1b..c2ecdf33e 100644 --- a/src/structuretoolkit/_version.py +++ b/src/structuretoolkit/_version.py @@ -18,7 +18,7 @@ commit_id: str | None __commit_id__: str | None -__version__ = version = '0.1.dev1+gd17555710.d20260403' -__version_tuple__ = version_tuple = (0, 1, 'dev1', 'gd17555710.d20260403') +__version__ = version = "0.1.dev1+gd17555710.d20260403" +__version_tuple__ = version_tuple = (0, 1, "dev1", "gd17555710.d20260403") -__commit_id__ = commit_id = None \ No newline at end of file +__commit_id__ = commit_id = None From 742681d49562266965791fd33629b4856ef52c09 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:56:49 +0000 Subject: [PATCH 10/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/structuretoolkit/build/materialsproject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structuretoolkit/build/materialsproject.py b/src/structuretoolkit/build/materialsproject.py index c1fbba5f6..742912769 100644 --- a/src/structuretoolkit/build/materialsproject.py +++ b/src/structuretoolkit/build/materialsproject.py @@ -135,4 +135,4 @@ def by_id( conventional_unit_cell=conventional_unit_cell, ) ) - ] \ No newline at end of file + ] From 08c2a0f06eeb12f8e63fcefe57ebf1849a1691e9 Mon Sep 17 00:00:00 2001 From: Jan Janssen Date: Mon, 6 Apr 2026 09:14:17 +0200 Subject: [PATCH 11/17] Change versioning from 0.1.dev1 to 0.0.1 Updated versioning information and removed unused variables. --- src/structuretoolkit/_version.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/structuretoolkit/_version.py b/src/structuretoolkit/_version.py index c2ecdf33e..1a870a8a4 100644 --- a/src/structuretoolkit/_version.py +++ b/src/structuretoolkit/_version.py @@ -1,24 +1,20 @@ -# file generated by vcs-versioning +# file generated by setuptools-scm # don't change, don't track in version control -from __future__ import annotations -__all__ = [ - "__version__", - "__version_tuple__", - "version", - "version_tuple", - "__commit_id__", - "commit_id", -] +__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] + +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple, Union + + VERSION_TUPLE = Tuple[Union[int, str], ...] +else: + VERSION_TUPLE = object version: str __version__: str -__version_tuple__: tuple[int | str, ...] -version_tuple: tuple[int | str, ...] -commit_id: str | None -__commit_id__: str | None - -__version__ = version = "0.1.dev1+gd17555710.d20260403" -__version_tuple__ = version_tuple = (0, 1, "dev1", "gd17555710.d20260403") +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE -__commit_id__ = commit_id = None +__version__ = version = "0.0.1" +__version_tuple__ = version_tuple = (0, 0, 1) From fe07fb103d897b8f075146e9da0e7622ce51e172 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Mon, 6 Apr 2026 03:42:11 -0400 Subject: [PATCH 12/17] Add tests for materialsproject Generated with Claude. --- tests/test_materialsproject.py | 260 +++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 tests/test_materialsproject.py diff --git a/tests/test_materialsproject.py b/tests/test_materialsproject.py new file mode 100644 index 000000000..88c9a173c --- /dev/null +++ b/tests/test_materialsproject.py @@ -0,0 +1,260 @@ +import unittest +from unittest.mock import MagicMock, patch + +import numpy as np +from ase.atoms import Atoms +from ase.build import bulk + +from structuretoolkit.build.materialsproject import by_id, search + + +def _make_pymatgen_structure(ase_atoms): + """Convert ASE Atoms to a pymatgen Structure for use as mock return value.""" + from pymatgen.io.ase import AseAtomsAdaptor + + return AseAtomsAdaptor().get_structure(atoms=ase_atoms) + + +class TestMaterialsProjectSearch(unittest.TestCase): + def setUp(self): + self.fe_bcc = bulk("Fe", "bcc", a=2.87) + self.al_fcc = bulk("Al", "fcc", a=4.05) + self.fe_pmg = _make_pymatgen_structure(self.fe_bcc) + self.al_pmg = _make_pymatgen_structure(self.al_fcc) + + @patch("mp_api.client.MPRester") + def test_search_single_chemsys(self, MockMPRester): + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.summary.search.return_value = [ + {"material_id": "mp-13", "structure": self.fe_pmg}, + ] + + results = list(search("Fe")) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["material_id"], "mp-13") + self.assertIsInstance(results[0]["structure"], Atoms) + self.assertEqual(results[0]["structure"].get_chemical_symbols(), ["Fe"]) + + mock_mpr.summary.search.assert_called_once_with( + chemsys="Fe", + fields=["structure", "material_id"], + ) + + @patch("mp_api.client.MPRester") + def test_search_multiple_chemsys(self, MockMPRester): + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.summary.search.return_value = [ + {"material_id": "mp-13", "structure": self.fe_pmg}, + {"material_id": "mp-134", "structure": self.al_pmg}, + ] + + results = list(search(["Fe", "Al"])) + + self.assertEqual(len(results), 2) + self.assertEqual(results[0]["material_id"], "mp-13") + self.assertEqual(results[1]["material_id"], "mp-134") + for r in results: + self.assertIsInstance(r["structure"], Atoms) + + @patch("mp_api.client.MPRester") + def test_search_with_extra_fields(self, MockMPRester): + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.summary.search.return_value = [ + { + "material_id": "mp-13", + "structure": self.fe_pmg, + "energy_above_hull": 0.0, + }, + ] + + results = list(search("Fe", fields=["energy_above_hull"])) + + self.assertEqual(results[0]["energy_above_hull"], 0.0) + mock_mpr.summary.search.assert_called_once_with( + chemsys="Fe", + fields=["energy_above_hull", "structure", "material_id"], + ) + + @patch("mp_api.client.MPRester") + def test_search_with_kwargs(self, MockMPRester): + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.summary.search.return_value = [ + {"material_id": "mp-13", "structure": self.fe_pmg}, + ] + + list(search("Fe", is_stable=True)) + + mock_mpr.summary.search.assert_called_once_with( + chemsys="Fe", + is_stable=True, + fields=["structure", "material_id"], + ) + + @patch("mp_api.client.MPRester") + def test_search_with_api_key(self, MockMPRester): + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.summary.search.return_value = [] + + list(search("Fe", api_key="test-key-123")) + + MockMPRester.assert_called_once_with( + use_document_model=False, + include_user_agent=True, + api_key="test-key-123", + ) + + @patch("mp_api.client.MPRester") + def test_search_without_api_key(self, MockMPRester): + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.summary.search.return_value = [] + + list(search("Fe")) + + MockMPRester.assert_called_once_with( + use_document_model=False, + include_user_agent=True, + ) + + @patch("mp_api.client.MPRester") + def test_search_empty_results(self, MockMPRester): + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.summary.search.return_value = [] + + results = list(search("Uuo")) + + self.assertEqual(len(results), 0) + + @patch("mp_api.client.MPRester") + def test_search_is_generator(self, MockMPRester): + """search() should yield results lazily.""" + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.summary.search.return_value = [ + {"material_id": "mp-13", "structure": self.fe_pmg}, + ] + + gen = search("Fe") + import types + + self.assertIsInstance(gen, types.GeneratorType) + + +class TestMaterialsProjectById(unittest.TestCase): + def setUp(self): + self.fe_bcc = bulk("Fe", "bcc", a=2.87) + self.fe_pmg = _make_pymatgen_structure(self.fe_bcc) + + @patch("mp_api.client.MPRester") + def test_by_id_final(self, MockMPRester): + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.get_structure_by_material_id.return_value = self.fe_pmg + + result = by_id("mp-13") + + self.assertIsInstance(result, Atoms) + self.assertEqual(result.get_chemical_symbols(), ["Fe"]) + mock_mpr.get_structure_by_material_id.assert_called_once_with( + material_id="mp-13", + final=True, + conventional_unit_cell=False, + ) + + @patch("mp_api.client.MPRester") + def test_by_id_not_final(self, MockMPRester): + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.get_structure_by_material_id.return_value = [ + self.fe_pmg, + self.fe_pmg, + ] + + result = by_id("mp-13", final=False) + + self.assertIsInstance(result, list) + self.assertEqual(len(result), 2) + for atoms in result: + self.assertIsInstance(atoms, Atoms) + mock_mpr.get_structure_by_material_id.assert_called_once_with( + material_id="mp-13", + final=False, + conventional_unit_cell=False, + ) + + @patch("mp_api.client.MPRester") + def test_by_id_conventional_unit_cell(self, MockMPRester): + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.get_structure_by_material_id.return_value = self.fe_pmg + + by_id("mp-13", conventional_unit_cell=True) + + mock_mpr.get_structure_by_material_id.assert_called_once_with( + material_id="mp-13", + final=True, + conventional_unit_cell=True, + ) + + @patch("mp_api.client.MPRester") + def test_by_id_with_api_key(self, MockMPRester): + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.get_structure_by_material_id.return_value = self.fe_pmg + + by_id("mp-13", api_key="test-key-456") + + MockMPRester.assert_called_once_with( + include_user_agent=True, + api_key="test-key-456", + ) + + @patch("mp_api.client.MPRester") + def test_by_id_without_api_key(self, MockMPRester): + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.get_structure_by_material_id.return_value = self.fe_pmg + + by_id("mp-13") + + MockMPRester.assert_called_once_with( + include_user_agent=True, + ) + + @patch("mp_api.client.MPRester") + def test_by_id_structure_has_correct_cell(self, MockMPRester): + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.get_structure_by_material_id.return_value = self.fe_pmg + + result = by_id("mp-13") + + self.assertTrue( + np.allclose(result.cell.array, self.fe_bcc.cell.array, atol=1e-6), + "Cell parameters should be preserved through pymatgen conversion.", + ) + self.assertTrue( + np.all(result.pbc), + "Periodic boundary conditions should be set.", + ) From a36f061abc03e8254dcd013cced9a2ef74f363b0 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Mon, 6 Apr 2026 03:44:41 -0400 Subject: [PATCH 13/17] Add pymatgen to mp-api opt deps --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1280eff0a..4393a326b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,10 @@ phonopy = [ "phonopy==3.3.0", "spglib==2.7.0", ] -mp-api = ["mp-api==0.37.2"] +mp-api = [ + "mp-api==0.37.2", + "pymatgen==2026.3.23", +] [tool.ruff] exclude = [".ci_support", "tests", "setup.py", "_version.py"] From d3b9125af8267ae69d5078e2da0bdd8ca8d47963 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Mon, 6 Apr 2026 03:47:02 -0400 Subject: [PATCH 14/17] Skip materialsproject tests when mp-api and pymatgen are not installed Co-Authored-By: Claude Opus 4.6 --- tests/test_materialsproject.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_materialsproject.py b/tests/test_materialsproject.py index 88c9a173c..51c7532db 100644 --- a/tests/test_materialsproject.py +++ b/tests/test_materialsproject.py @@ -1,10 +1,18 @@ +import importlib import unittest from unittest.mock import MagicMock, patch +import pytest import numpy as np from ase.atoms import Atoms from ase.build import bulk +pytestmark = pytest.mark.skipif( + importlib.util.find_spec("mp_api") is None + and importlib.util.find_spec("pymatgen") is None, + reason="mp-api and pymatgen are not installed", +) + from structuretoolkit.build.materialsproject import by_id, search From 211a776555d40327b82e3dbf3788e58f9e99e473 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Mon, 6 Apr 2026 12:53:10 -0400 Subject: [PATCH 15/17] Convert pytest skipif to stdlib unittest in materialsproject tests Replace pytest.mark.skipif with unittest.SkipTest in setUpModule() to skip the entire test module when mp-api and pymatgen are not installed. Co-Authored-By: Claude Haiku 4.5 --- tests/test_materialsproject.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/test_materialsproject.py b/tests/test_materialsproject.py index 51c7532db..d989370ba 100644 --- a/tests/test_materialsproject.py +++ b/tests/test_materialsproject.py @@ -2,20 +2,22 @@ import unittest from unittest.mock import MagicMock, patch -import pytest import numpy as np from ase.atoms import Atoms from ase.build import bulk -pytestmark = pytest.mark.skipif( - importlib.util.find_spec("mp_api") is None - and importlib.util.find_spec("pymatgen") is None, - reason="mp-api and pymatgen are not installed", -) - from structuretoolkit.build.materialsproject import by_id, search +def setUpModule(): + """Skip the entire module if mp_api and pymatgen are not installed.""" + if ( + importlib.util.find_spec("mp_api") is None + and importlib.util.find_spec("pymatgen") is None + ): + raise unittest.SkipTest("mp-api and pymatgen are not installed") + + def _make_pymatgen_structure(ase_atoms): """Convert ASE Atoms to a pymatgen Structure for use as mock return value.""" from pymatgen.io.ase import AseAtomsAdaptor From 663154dc4a44fabde27805cc2a3bb2d2c55c3732 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Mon, 6 Apr 2026 13:04:54 -0400 Subject: [PATCH 16/17] Fix materialsproject tests to skip cleanly when dependencies missing Convert @patch decorators to context managers (with patch(...)) so that patch resolution happens at test execution time, after setUpModule() has run. This allows the skip to work properly in unittest.discover. Previously, the decorators tried to resolve mp_api at decoration time, which happens before setUpModule() could skip the module, causing errors in environments without mp-api installed. Co-Authored-By: Claude Haiku 4.5 --- tests/test_materialsproject.py | 452 ++++++++++++++++----------------- 1 file changed, 226 insertions(+), 226 deletions(-) diff --git a/tests/test_materialsproject.py b/tests/test_materialsproject.py index d989370ba..d11bd5c94 100644 --- a/tests/test_materialsproject.py +++ b/tests/test_materialsproject.py @@ -32,137 +32,137 @@ def setUp(self): self.fe_pmg = _make_pymatgen_structure(self.fe_bcc) self.al_pmg = _make_pymatgen_structure(self.al_fcc) - @patch("mp_api.client.MPRester") - def test_search_single_chemsys(self, MockMPRester): - mock_mpr = MagicMock() - MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) - MockMPRester.return_value.__exit__ = MagicMock(return_value=False) - mock_mpr.summary.search.return_value = [ - {"material_id": "mp-13", "structure": self.fe_pmg}, - ] - - results = list(search("Fe")) - - self.assertEqual(len(results), 1) - self.assertEqual(results[0]["material_id"], "mp-13") - self.assertIsInstance(results[0]["structure"], Atoms) - self.assertEqual(results[0]["structure"].get_chemical_symbols(), ["Fe"]) - - mock_mpr.summary.search.assert_called_once_with( - chemsys="Fe", - fields=["structure", "material_id"], - ) - - @patch("mp_api.client.MPRester") - def test_search_multiple_chemsys(self, MockMPRester): - mock_mpr = MagicMock() - MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) - MockMPRester.return_value.__exit__ = MagicMock(return_value=False) - mock_mpr.summary.search.return_value = [ - {"material_id": "mp-13", "structure": self.fe_pmg}, - {"material_id": "mp-134", "structure": self.al_pmg}, - ] - - results = list(search(["Fe", "Al"])) - - self.assertEqual(len(results), 2) - self.assertEqual(results[0]["material_id"], "mp-13") - self.assertEqual(results[1]["material_id"], "mp-134") - for r in results: - self.assertIsInstance(r["structure"], Atoms) - - @patch("mp_api.client.MPRester") - def test_search_with_extra_fields(self, MockMPRester): - mock_mpr = MagicMock() - MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) - MockMPRester.return_value.__exit__ = MagicMock(return_value=False) - mock_mpr.summary.search.return_value = [ - { - "material_id": "mp-13", - "structure": self.fe_pmg, - "energy_above_hull": 0.0, - }, - ] - - results = list(search("Fe", fields=["energy_above_hull"])) - - self.assertEqual(results[0]["energy_above_hull"], 0.0) - mock_mpr.summary.search.assert_called_once_with( - chemsys="Fe", - fields=["energy_above_hull", "structure", "material_id"], - ) - - @patch("mp_api.client.MPRester") - def test_search_with_kwargs(self, MockMPRester): - mock_mpr = MagicMock() - MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) - MockMPRester.return_value.__exit__ = MagicMock(return_value=False) - mock_mpr.summary.search.return_value = [ - {"material_id": "mp-13", "structure": self.fe_pmg}, - ] - - list(search("Fe", is_stable=True)) - - mock_mpr.summary.search.assert_called_once_with( - chemsys="Fe", - is_stable=True, - fields=["structure", "material_id"], - ) - - @patch("mp_api.client.MPRester") - def test_search_with_api_key(self, MockMPRester): - mock_mpr = MagicMock() - MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) - MockMPRester.return_value.__exit__ = MagicMock(return_value=False) - mock_mpr.summary.search.return_value = [] - - list(search("Fe", api_key="test-key-123")) - - MockMPRester.assert_called_once_with( - use_document_model=False, - include_user_agent=True, - api_key="test-key-123", - ) - - @patch("mp_api.client.MPRester") - def test_search_without_api_key(self, MockMPRester): - mock_mpr = MagicMock() - MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) - MockMPRester.return_value.__exit__ = MagicMock(return_value=False) - mock_mpr.summary.search.return_value = [] - - list(search("Fe")) - - MockMPRester.assert_called_once_with( - use_document_model=False, - include_user_agent=True, - ) - - @patch("mp_api.client.MPRester") - def test_search_empty_results(self, MockMPRester): - mock_mpr = MagicMock() - MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) - MockMPRester.return_value.__exit__ = MagicMock(return_value=False) - mock_mpr.summary.search.return_value = [] - - results = list(search("Uuo")) - - self.assertEqual(len(results), 0) - - @patch("mp_api.client.MPRester") - def test_search_is_generator(self, MockMPRester): + def test_search_single_chemsys(self): + with patch("mp_api.client.MPRester") as MockMPRester: + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.summary.search.return_value = [ + {"material_id": "mp-13", "structure": self.fe_pmg}, + ] + + results = list(search("Fe")) + + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["material_id"], "mp-13") + self.assertIsInstance(results[0]["structure"], Atoms) + self.assertEqual(results[0]["structure"].get_chemical_symbols(), ["Fe"]) + + mock_mpr.summary.search.assert_called_once_with( + chemsys="Fe", + fields=["structure", "material_id"], + ) + + def test_search_multiple_chemsys(self): + with patch("mp_api.client.MPRester") as MockMPRester: + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.summary.search.return_value = [ + {"material_id": "mp-13", "structure": self.fe_pmg}, + {"material_id": "mp-134", "structure": self.al_pmg}, + ] + + results = list(search(["Fe", "Al"])) + + self.assertEqual(len(results), 2) + self.assertEqual(results[0]["material_id"], "mp-13") + self.assertEqual(results[1]["material_id"], "mp-134") + for r in results: + self.assertIsInstance(r["structure"], Atoms) + + def test_search_with_extra_fields(self): + with patch("mp_api.client.MPRester") as MockMPRester: + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.summary.search.return_value = [ + { + "material_id": "mp-13", + "structure": self.fe_pmg, + "energy_above_hull": 0.0, + }, + ] + + results = list(search("Fe", fields=["energy_above_hull"])) + + self.assertEqual(results[0]["energy_above_hull"], 0.0) + mock_mpr.summary.search.assert_called_once_with( + chemsys="Fe", + fields=["energy_above_hull", "structure", "material_id"], + ) + + def test_search_with_kwargs(self): + with patch("mp_api.client.MPRester") as MockMPRester: + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.summary.search.return_value = [ + {"material_id": "mp-13", "structure": self.fe_pmg}, + ] + + list(search("Fe", is_stable=True)) + + mock_mpr.summary.search.assert_called_once_with( + chemsys="Fe", + is_stable=True, + fields=["structure", "material_id"], + ) + + def test_search_with_api_key(self): + with patch("mp_api.client.MPRester") as MockMPRester: + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.summary.search.return_value = [] + + list(search("Fe", api_key="test-key-123")) + + MockMPRester.assert_called_once_with( + use_document_model=False, + include_user_agent=True, + api_key="test-key-123", + ) + + def test_search_without_api_key(self): + with patch("mp_api.client.MPRester") as MockMPRester: + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.summary.search.return_value = [] + + list(search("Fe")) + + MockMPRester.assert_called_once_with( + use_document_model=False, + include_user_agent=True, + ) + + def test_search_empty_results(self): + with patch("mp_api.client.MPRester") as MockMPRester: + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.summary.search.return_value = [] + + results = list(search("Uuo")) + + self.assertEqual(len(results), 0) + + def test_search_is_generator(self): """search() should yield results lazily.""" - mock_mpr = MagicMock() - MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) - MockMPRester.return_value.__exit__ = MagicMock(return_value=False) - mock_mpr.summary.search.return_value = [ - {"material_id": "mp-13", "structure": self.fe_pmg}, - ] + with patch("mp_api.client.MPRester") as MockMPRester: + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.summary.search.return_value = [ + {"material_id": "mp-13", "structure": self.fe_pmg}, + ] - gen = search("Fe") - import types + gen = search("Fe") + import types - self.assertIsInstance(gen, types.GeneratorType) + self.assertIsInstance(gen, types.GeneratorType) class TestMaterialsProjectById(unittest.TestCase): @@ -170,101 +170,101 @@ def setUp(self): self.fe_bcc = bulk("Fe", "bcc", a=2.87) self.fe_pmg = _make_pymatgen_structure(self.fe_bcc) - @patch("mp_api.client.MPRester") - def test_by_id_final(self, MockMPRester): - mock_mpr = MagicMock() - MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) - MockMPRester.return_value.__exit__ = MagicMock(return_value=False) - mock_mpr.get_structure_by_material_id.return_value = self.fe_pmg - - result = by_id("mp-13") - - self.assertIsInstance(result, Atoms) - self.assertEqual(result.get_chemical_symbols(), ["Fe"]) - mock_mpr.get_structure_by_material_id.assert_called_once_with( - material_id="mp-13", - final=True, - conventional_unit_cell=False, - ) - - @patch("mp_api.client.MPRester") - def test_by_id_not_final(self, MockMPRester): - mock_mpr = MagicMock() - MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) - MockMPRester.return_value.__exit__ = MagicMock(return_value=False) - mock_mpr.get_structure_by_material_id.return_value = [ - self.fe_pmg, - self.fe_pmg, - ] - - result = by_id("mp-13", final=False) - - self.assertIsInstance(result, list) - self.assertEqual(len(result), 2) - for atoms in result: - self.assertIsInstance(atoms, Atoms) - mock_mpr.get_structure_by_material_id.assert_called_once_with( - material_id="mp-13", - final=False, - conventional_unit_cell=False, - ) - - @patch("mp_api.client.MPRester") - def test_by_id_conventional_unit_cell(self, MockMPRester): - mock_mpr = MagicMock() - MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) - MockMPRester.return_value.__exit__ = MagicMock(return_value=False) - mock_mpr.get_structure_by_material_id.return_value = self.fe_pmg - - by_id("mp-13", conventional_unit_cell=True) - - mock_mpr.get_structure_by_material_id.assert_called_once_with( - material_id="mp-13", - final=True, - conventional_unit_cell=True, - ) - - @patch("mp_api.client.MPRester") - def test_by_id_with_api_key(self, MockMPRester): - mock_mpr = MagicMock() - MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) - MockMPRester.return_value.__exit__ = MagicMock(return_value=False) - mock_mpr.get_structure_by_material_id.return_value = self.fe_pmg - - by_id("mp-13", api_key="test-key-456") - - MockMPRester.assert_called_once_with( - include_user_agent=True, - api_key="test-key-456", - ) - - @patch("mp_api.client.MPRester") - def test_by_id_without_api_key(self, MockMPRester): - mock_mpr = MagicMock() - MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) - MockMPRester.return_value.__exit__ = MagicMock(return_value=False) - mock_mpr.get_structure_by_material_id.return_value = self.fe_pmg - - by_id("mp-13") - - MockMPRester.assert_called_once_with( - include_user_agent=True, - ) - - @patch("mp_api.client.MPRester") - def test_by_id_structure_has_correct_cell(self, MockMPRester): - mock_mpr = MagicMock() - MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) - MockMPRester.return_value.__exit__ = MagicMock(return_value=False) - mock_mpr.get_structure_by_material_id.return_value = self.fe_pmg - - result = by_id("mp-13") - - self.assertTrue( - np.allclose(result.cell.array, self.fe_bcc.cell.array, atol=1e-6), - "Cell parameters should be preserved through pymatgen conversion.", - ) - self.assertTrue( - np.all(result.pbc), - "Periodic boundary conditions should be set.", - ) + def test_by_id_final(self): + with patch("mp_api.client.MPRester") as MockMPRester: + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.get_structure_by_material_id.return_value = self.fe_pmg + + result = by_id("mp-13") + + self.assertIsInstance(result, Atoms) + self.assertEqual(result.get_chemical_symbols(), ["Fe"]) + mock_mpr.get_structure_by_material_id.assert_called_once_with( + material_id="mp-13", + final=True, + conventional_unit_cell=False, + ) + + def test_by_id_not_final(self): + with patch("mp_api.client.MPRester") as MockMPRester: + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.get_structure_by_material_id.return_value = [ + self.fe_pmg, + self.fe_pmg, + ] + + result = by_id("mp-13", final=False) + + self.assertIsInstance(result, list) + self.assertEqual(len(result), 2) + for atoms in result: + self.assertIsInstance(atoms, Atoms) + mock_mpr.get_structure_by_material_id.assert_called_once_with( + material_id="mp-13", + final=False, + conventional_unit_cell=False, + ) + + def test_by_id_conventional_unit_cell(self): + with patch("mp_api.client.MPRester") as MockMPRester: + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.get_structure_by_material_id.return_value = self.fe_pmg + + by_id("mp-13", conventional_unit_cell=True) + + mock_mpr.get_structure_by_material_id.assert_called_once_with( + material_id="mp-13", + final=True, + conventional_unit_cell=True, + ) + + def test_by_id_with_api_key(self): + with patch("mp_api.client.MPRester") as MockMPRester: + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.get_structure_by_material_id.return_value = self.fe_pmg + + by_id("mp-13", api_key="test-key-456") + + MockMPRester.assert_called_once_with( + include_user_agent=True, + api_key="test-key-456", + ) + + def test_by_id_without_api_key(self): + with patch("mp_api.client.MPRester") as MockMPRester: + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.get_structure_by_material_id.return_value = self.fe_pmg + + by_id("mp-13") + + MockMPRester.assert_called_once_with( + include_user_agent=True, + ) + + def test_by_id_structure_has_correct_cell(self): + with patch("mp_api.client.MPRester") as MockMPRester: + mock_mpr = MagicMock() + MockMPRester.return_value.__enter__ = MagicMock(return_value=mock_mpr) + MockMPRester.return_value.__exit__ = MagicMock(return_value=False) + mock_mpr.get_structure_by_material_id.return_value = self.fe_pmg + + result = by_id("mp-13") + + self.assertTrue( + np.allclose(result.cell.array, self.fe_bcc.cell.array, atol=1e-6), + "Cell parameters should be preserved through pymatgen conversion.", + ) + self.assertTrue( + np.all(result.pbc), + "Periodic boundary conditions should be set.", + ) From ed40b639e6323f935c6ba1705e17173abf01488a Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Mon, 6 Apr 2026 17:09:40 +0000 Subject: [PATCH 17/17] Fail if either dep is missing --- tests/test_materialsproject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_materialsproject.py b/tests/test_materialsproject.py index d11bd5c94..7f9bcf1d0 100644 --- a/tests/test_materialsproject.py +++ b/tests/test_materialsproject.py @@ -13,7 +13,7 @@ def setUpModule(): """Skip the entire module if mp_api and pymatgen are not installed.""" if ( importlib.util.find_spec("mp_api") is None - and importlib.util.find_spec("pymatgen") is None + or importlib.util.find_spec("pymatgen") is None ): raise unittest.SkipTest("mp-api and pymatgen are not installed")