Skip to content

Commit 4af3a61

Browse files
revise find structure method to push more work to user
1 parent e65b620 commit 4af3a61

7 files changed

Lines changed: 77 additions & 42 deletions

File tree

mp_api/client/core/client.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1617,9 +1617,7 @@ def __getattr__(self, v: str):
16171617
force_renew=self.force_renew,
16181618
)
16191619
return self.sub_resters[v]
1620-
raise AttributeError(
1621-
f"{self.__class__} has no attribute {v}"
1622-
)
1620+
raise AttributeError(f"{self.__class__} has no attribute {v}")
16231621

16241622
def __dir__(self):
16251623
return dir(self.__class__) + list(self._sub_resters)

mp_api/client/routes/materials/materials.py

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from pathlib import Path
34
from typing import TYPE_CHECKING
45

56
from emmet.core.symmetry import CrystalSystem
@@ -172,11 +173,11 @@ def search(
172173

173174
def find_structure(
174175
self,
175-
filename_or_structure,
176+
filename_or_structure: str | Path | Structure,
176177
ltol=MAPI_CLIENT_SETTINGS.LTOL,
177178
stol=MAPI_CLIENT_SETTINGS.STOL,
178179
angle_tol=MAPI_CLIENT_SETTINGS.ANGLE_TOL,
179-
allow_multiple_results=False,
180+
allow_multiple_results: bool | int = False,
180181
) -> list[str] | str:
181182
"""Finds matching structures from the Materials Project database.
182183
@@ -186,48 +187,72 @@ def find_structure(
186187
default tolerances.
187188
188189
Args:
189-
filename_or_structure: filename or Structure object
190+
filename_or_structure: filename as a str or Path, or a Structure object
190191
ltol: fractional length tolerance
191192
stol: site tolerance
192193
angle_tol: angle tolerance in degrees
193-
allow_multiple_results: changes return type for either
194-
a single material_id or list of material_ids
194+
allow_multiple_results (bool or int): changes return type for either
195+
a single material_id or list of material_ids.
196+
If a bool, returns either all matches (True) or one match at most (False).
197+
If an int, returns that many matches at most.
198+
195199
Returns:
196200
A matching material_id if one is found or list of results if allow_multiple_results
197201
is True
198202
Raises:
199203
MPRestError
200204
"""
201-
params = {"ltol": ltol, "stol": stol, "angle_tol": angle_tol, "_limit": 1}
205+
from pymatgen.analysis.structure_matcher import (
206+
ElementComparator,
207+
StructureMatcher,
208+
)
202209

203-
if isinstance(filename_or_structure, str):
210+
if (
211+
isinstance(filename_or_structure, str | Path)
212+
and Path(filename_or_structure).exists()
213+
):
204214
s = Structure.from_file(filename_or_structure)
205215
elif isinstance(filename_or_structure, Structure):
206216
s = filename_or_structure
207217
else:
208218
raise MPRestError("Provide filename or Structure object.")
209219

210-
results = self._post_resource(
211-
body=s.as_dict(),
212-
params=params,
213-
suburl="find_structure",
214-
use_document_model=False,
215-
).get("data")
216-
217-
if not results:
220+
mat_docs = self.search(
221+
formula=s.reduced_formula, fields=["material_id", "structure"]
222+
)
223+
if not mat_docs:
218224
return []
219225

220-
material_ids = validate_ids([doc["material_id"] for doc in results])
226+
if isinstance(allow_multiple_results, bool):
227+
max_matches: int = len(mat_docs) if allow_multiple_results else 1
228+
elif isinstance(allow_multiple_results, int):
229+
max_matches = allow_multiple_results
230+
else:
231+
raise MPRestError(
232+
f"`allow_multiple_results` must be a bool or int, not {type(allow_multiple_results)}"
233+
)
234+
235+
matcher = StructureMatcher(
236+
ltol=ltol,
237+
stol=stol,
238+
angle_tol=angle_tol,
239+
primitive_cell=True,
240+
scale=True,
241+
attempt_supercell=False,
242+
comparator=ElementComparator(),
243+
)
221244

222-
if len(material_ids) > 1: # type: ignore
223-
if not allow_multiple_results:
224-
raise ValueError(
225-
"Multiple matches found for this combination of tolerances, but "
226-
"`allow_multiple_results` set to False."
227-
)
228-
return material_ids # type: ignore
245+
matches = []
246+
for doc in mat_docs:
247+
if matcher.fit(s, doc.structure):
248+
matches.append(doc.material_id)
249+
if len(matches) >= max_matches:
250+
break
229251

230-
return material_ids[0]
252+
if not matches:
253+
return []
254+
material_ids = validate_ids(matches)
255+
return material_ids if allow_multiple_results else material_ids[0]
231256

232257
def get_blessed_entries(
233258
self,

mp_api/client/routes/molecules/molecules.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,15 @@
77

88
from __future__ import annotations
99

10-
from emmet.core.mpid import MPculeID
1110
from emmet.core.qchem.molecule import MoleculeDoc
12-
from pymatgen.core.structure import Molecule
1311

14-
from mp_api.client.core.client import CoreRester, MPRestError
15-
from mp_api.client.core.utils import validate_ids
12+
from mp_api.client.core.client import CoreRester
1613
from mp_api.client.routes.molecules import MOLECULES_RESTERS
1714

15+
1816
class MoleculeRester(CoreRester):
1917
"""Define molecules stub for accessing JCESR and summary data."""
18+
2019
document_model = MoleculeDoc
2120
primary_key = "molecule_id"
2221
suffix = "molecules/core"

mp_api/client/routes/molecules/summary.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def search(
4343
formula (str, List[str]): An alphabetical formula or list of formulas
4444
(e.g. "C2 Li2 O4", ["C2 H4", "C2 H6"]).
4545
has_props: (List[HasProps]): The calculated properties available for the material.
46-
molecule_ids (str or MPculeID, or list[str | MPculeID]):
46+
molecule_ids (str or MPculeID, or list[str | MPculeID]):
4747
(List of) Materials Project Molecule IDs (MPculeIDs) to return data for.
4848
num_chunks (int): Maximum number of chunks of data to yield. None will yield all possible.
4949
chunk_size (int): Number of data entries per chunk.
@@ -76,7 +76,7 @@ def search(
7676
)
7777

7878
if molecule_ids:
79-
if isinstance(molecule_ids,str):
79+
if isinstance(molecule_ids, str):
8080
molecule_ids = [molecule_ids]
8181
query_params.update({"molecule_ids": ",".join(molecule_ids)})
8282

@@ -119,4 +119,4 @@ def search(
119119
all_fields=all_fields,
120120
fields=fields,
121121
**query_params,
122-
)
122+
)

tests/client/molecules/test_summary.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"molecule_ids": [
2626
"042b6da7a6eb790fd5038f3729ef715c-C5H8O3-m1-2",
2727
"6fef3b0ef7f484b8c2dd0a0012bf7c70-C2H2O3P1-0-2",
28-
"a3572e662965c190b2a085c6a67a8a03-C9H12-1-2"
28+
"a3572e662965c190b2a085c6a67a8a03-C9H12-1-2",
2929
],
3030
"formula": "C2 H4",
3131
"chemsys": "C-H",

tests/client/test_core_client.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,6 @@ def mpr():
2020
rester.session.close()
2121

2222

23-
@requires_api_key
24-
@pytest.mark.xfail
25-
def test_post_fail(rester):
26-
rester._post_resource({}, suburl="materials/find_structure")
27-
28-
2923
@requires_api_key
3024
def test_pagination(mpr):
3125
mpids = mpr.materials.search(

tests/client/test_mprester.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,20 +131,39 @@ def test_find_structure(self, mpr):
131131
_atom_site_occupancy
132132
Ne Ne0 1 0.00000000 0.00000000 -0.00000000 1
133133
"""
134+
struct_from_cif = CifParser.from_str(cif_str).parse_structures(primitive=True)[
135+
0
136+
]
134137
temp_file = NamedTemporaryFile(suffix=".cif")
135138
with open(temp_file.name, "wt") as f:
136139
f.write(cif_str)
137140
f.seek(0)
138141

139142
for struct_or_path in (
140143
temp_file.name,
141-
CifParser.from_str(cif_str).parse_structures(primitive=True)[0],
144+
struct_from_cif,
142145
):
143146
data = mpr.find_structure(struct_or_path)
144147
assert isinstance(data, str) and data == "mp-111"
145148

146149
f.close()
147150

151+
with pytest.raises(MPRestError, match="Provide filename or Structure object."):
152+
mpr.find_structure(struct_from_cif.as_dict())
153+
154+
with pytest.raises(MPRestError, match="`allow_multiple_results` must be a"):
155+
mpr.find_structure(struct_from_cif, allow_multiple_results=1.0)
156+
157+
assert (
158+
len(
159+
mpr.find_structure(
160+
struct_from_cif.copy().replace_species({"Ne": "K"}),
161+
allow_multiple_results=2,
162+
)
163+
)
164+
<= 2
165+
)
166+
148167
def test_get_bandstructure_by_material_id(self, mpr):
149168
bs = mpr.get_bandstructure_by_material_id("mp-149")
150169
assert isinstance(bs, BandStructureSymmLine)

0 commit comments

Comments
 (0)