Skip to content

Commit 3c2a7b4

Browse files
Merge remote-tracking branch 'origin/main' into contribs
2 parents 603b80f + 1d25d67 commit 3c2a7b4

5 files changed

Lines changed: 110 additions & 50 deletions

File tree

mp_api/client/core/client.py

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ def _query_resource(
640640
use_document_model: if None, will defer to the self.use_document_model attribute
641641
num_chunks: Maximum number of chunks of data to yield. None will yield all possible.
642642
chunk_size: Number of data entries per chunk.
643-
timeout : Time in seconds to wait until a request timeout error is thrown
643+
timeout (float or None): Time in seconds to wait until a request timeout error is thrown
644644
645645
Returns:
646646
A Resource, a dict with two keys, "data" containing a list of documents, and
@@ -786,27 +786,32 @@ def _query_resource(
786786
except RequestException as ex:
787787
raise MPRestError(str(ex))
788788

789-
def _submit_requests( # noqa
789+
def _submit_requests(
790790
self,
791-
url,
792-
criteria,
793-
use_document_model,
794-
chunk_size,
795-
num_chunks=None,
796-
timeout=None,
791+
url: str,
792+
criteria: dict[str, Any],
793+
use_document_model: bool,
794+
chunk_size: int | None,
795+
num_chunks: int | None = None,
796+
timeout: int | None = None,
797+
max_batch_size: int = 100,
798+
norecur: bool = False,
797799
) -> dict:
798800
"""Handle submitting requests sequentially with pagination.
799801
800802
If criteria contains comma-separated parameters (except those that are naturally comma-separated),
801803
split them into multiple sequential requests and combine results.
802804
803805
Arguments:
804-
criteria: dictionary of criteria to filter down
805-
url: url used to make request
806-
use_document_model: if None, will defer to the self.use_document_model attribute
807-
num_chunks: Maximum number of chunks of data to yield. None will yield all possible.
808-
chunk_size: Number of data entries per chunk.
809-
timeout: Time in seconds to wait until a request timeout error is thrown
806+
url (str): url used to make request
807+
criteria (dict of str): dictionary of criteria to filter down
808+
use_document_model (bool): whether to use the document model
809+
num_chunks (int or None): Maximum number of chunks of data to yield. None will yield all possible.
810+
chunk_size (int or None): Number of data entries per chunk.
811+
timeout (int or None): Time in seconds to wait until a request timeout error is thrown
812+
max_batch_size (int) : Maximum size of a batch when retrieving batches in parallel
813+
norecur (bool) : Whether to forbid recursive splitting of a query field
814+
when a direct query fails
810815
811816
Returns:
812817
Dictionary containing data and metadata
@@ -865,14 +870,6 @@ def _submit_requests( # noqa
865870
timeout=timeout,
866871
)
867872

868-
# Check if we got 0 results - some parameters are silently ignored by the API
869-
# when passed as comma-separated values, so we need to split them anyway
870-
if total_num_docs == 0 and len(split_values) > 1:
871-
# Treat this the same as a 422 error - split into batches
872-
raise MPRestError(
873-
"Got 0 results for comma-separated parameter, will try splitting"
874-
)
875-
876873
# If successful, continue with normal pagination
877874
data_chunks = [data["data"]]
878875
total_data: dict[str, Any] = {"data": []}
@@ -884,18 +881,26 @@ def _submit_requests( # noqa
884881
# Continue with pagination if needed (handled below)
885882

886883
except MPRestError as e:
887-
# If we get 422 or 414 error, or 0 results for comma-separated params, split into batches
888-
if any(trace in str(e) for trace in ("422", "414", "Got 0 results")):
884+
# If we get 422 or 414 error, split into batches
885+
if not norecur and any(
886+
trace in str(e)
887+
for trace in (
888+
"422",
889+
"414",
890+
)
891+
):
889892
total_data = {"data": []}
890893
total_num_docs = 0
891894
data_chunks = []
892895

893896
# Batch the split values to reduce number of requests
894897
# Use batches of up to 100 values to balance URL length and request count
895-
batch_size = min(100, max(1, len(split_values) // 10))
898+
num_batches = min(
899+
max_batch_size, max(1, len(split_values) // max_batch_size)
900+
)
901+
batch_size = min(len(split_values), max_batch_size)
896902

897903
# Setup progress bar for split parameter requests
898-
num_batches = ceil(len(split_values) / batch_size)
899904
pbar_message = f"Retrieving {len(split_values)} {split_param} values in {num_batches} batches"
900905
pbar = (
901906
tqdm(
@@ -919,6 +924,7 @@ def _submit_requests( # noqa
919924
chunk_size=chunk_size,
920925
num_chunks=num_chunks,
921926
timeout=timeout,
927+
norecur=len(batch) <= max_batch_size,
922928
)
923929

924930
data_chunks.append(result["data"])
@@ -960,6 +966,12 @@ def _submit_requests( # noqa
960966
if "meta" in data:
961967
total_data["meta"] = data["meta"]
962968

969+
# otherwise, paginate sequentially
970+
if chunk_size is None or chunk_size < 1:
971+
raise ValueError(
972+
"A positive chunk size must be provided to enable pagination"
973+
)
974+
963975
# Get max number of response pages
964976
max_pages = (
965977
num_chunks if num_chunks is not None else ceil(total_num_docs / chunk_size)
@@ -979,7 +991,7 @@ def _submit_requests( # noqa
979991
desc=pbar_message,
980992
total=num_docs_needed,
981993
)
982-
if not self.mute_progress_bars
994+
if not self.mute_progress_bars and total_num_docs > 0
983995
else None
984996
)
985997

@@ -999,10 +1011,6 @@ def _submit_requests( # noqa
9991011
pbar.close()
10001012
return new_total_data
10011013

1002-
# otherwise, paginate sequentially
1003-
if chunk_size is None:
1004-
raise ValueError("A chunk size must be provided to enable pagination")
1005-
10061014
# Warning to select specific fields only for many results
10071015
if criteria.get("_all_fields", False) and (total_num_docs / chunk_size > 10):
10081016
warnings.warn(

mp_api/client/mprester.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858

5959
from mp_api.client.core.client import _DictLikeAccess
6060

61-
DEFAULT_THERMOTYPE_CRITERIA = {"thermo_types": ["GGA_GGA+U"]}
61+
DEFAULT_THERMOTYPE_CRITERIA = {"thermo_types": ["GGA_GGA+U_R2SCAN"]}
6262

6363
RESTER_LAYOUT = {
6464
"molecules/core": LazyImport(
@@ -1048,7 +1048,7 @@ def get_entries_in_chemsys(
10481048
compatible_only: bool = True,
10491049
property_data: list[str] | None = None,
10501050
conventional_unit_cell: bool = False,
1051-
additional_criteria: dict = DEFAULT_THERMOTYPE_CRITERIA,
1051+
additional_criteria: dict | None = None,
10521052
**kwargs,
10531053
) -> list[ComputedStructureEntry] | list[GibbsComputedStructureEntry]:
10541054
"""Helper method to get a list of ComputedEntries in a chemical system.
@@ -1107,6 +1107,17 @@ def get_entries_in_chemsys(
11071107
for els in itertools.combinations(elements_set, i + 1)
11081108
]
11091109

1110+
if additional_criteria is None:
1111+
warnings.warn(
1112+
"The default thermo type when retrieving entries has been changed from "
1113+
"the mixed/corrected PBE GGA and GGA+U hull (`thermo_type = GGA_GGA+U`) "
1114+
"to the joint PBE GGA / GGA+U / r2SCAN hull (`thermo_type = GGA_GGA+U_R2SCAN`). "
1115+
"To use the older behavior, call `get_entries_in_chemsys` with "
1116+
'`additional_criteria = {"thermo_types": ["GGA_GGA+U"]}`',
1117+
category=MPRestWarning,
1118+
stacklevel=2,
1119+
)
1120+
11101121
entries = self.get_entries(
11111122
all_chemsyses,
11121123
compatible_only=compatible_only,
@@ -1364,23 +1375,17 @@ def get_download_info(
13641375
return meta, urls
13651376

13661377
def _check_get_download_info_url_by_task_id(self, prefix, task_ids) -> list[str]:
1367-
nomad_exist_task_ids: list[str] = []
13681378
prefix = prefix.replace("/raw/query", "/repo/")
1369-
for task_id in task_ids:
1370-
url = prefix + task_id
1371-
if self._check_nomad_exist(url):
1372-
nomad_exist_task_ids.append(task_id)
1373-
return nomad_exist_task_ids
1379+
return [
1380+
task_id for task_id in task_ids if self._check_nomad_exist(prefix + task_id)
1381+
]
13741382

13751383
@staticmethod
13761384
def _check_nomad_exist(url) -> bool:
13771385
response = get(url=url)
13781386
if response.status_code != 200:
13791387
return False
1380-
content = load_json(response.text)
1381-
if content["pagination"]["total"] == 0:
1382-
return False
1383-
return True
1388+
return load_json(response.text)["pagination"]["total"] != 0
13841389

13851390
@staticmethod
13861391
def _print_help_message(nomad_exist_task_ids, task_ids, file_patterns, calc_types):

mp_api/client/routes/materials/summary.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import warnings
44
from collections import defaultdict
5+
from itertools import chain, product
56

67
from emmet.core.summary import HasProps, SummaryDoc
78
from emmet.core.symmetry import CrystalSystem
@@ -200,8 +201,9 @@ def search( # noqa: D417
200201
mmnd_inv = {v: k for k, v in min_max_name_dict.items() if k != v}
201202

202203
# Set user query params from `locals`
204+
_locals = locals()
203205
user_settings = {
204-
k: v for k, v in locals().items() if k in min_max_name_dict and v
206+
k: v for k, v in _locals.items() if k in min_max_name_dict and v
205207
}
206208

207209
# Check to see if user specified _search fields using **kwargs,
@@ -328,10 +330,11 @@ def _csrc(x):
328330
"spacegroup_number": 230,
329331
"spacegroup_symbol": 230,
330332
}
333+
batched_symm_query = {}
331334
for k, cardinality in symm_cardinality.items():
332-
if isinstance(symm_vals := locals().get(k), list | tuple | set):
335+
if isinstance(symm_vals := _locals.get(k), list | tuple | set):
333336
if len(symm_vals) < cardinality // 2:
334-
query_params.update({k: ",".join(str(v) for v in symm_vals)})
337+
batched_symm_query[k] = symm_vals
335338
else:
336339
raise MPRestError(
337340
f"Querying `{k}` by a list of values is only "
@@ -378,6 +381,24 @@ def _csrc(x):
378381
if query_params[entry] is not None
379382
}
380383

384+
if batched_symm_query:
385+
ordered_symm_key = sorted(batched_symm_query)
386+
return list(
387+
chain.from_iterable(
388+
self._search( # type: ignore[return-value]
389+
num_chunks=num_chunks,
390+
chunk_size=chunk_size,
391+
all_fields=all_fields,
392+
fields=fields,
393+
**query_params,
394+
**{sk: symm_params[i] for i, sk in enumerate(ordered_symm_key)},
395+
)
396+
for symm_params in product(
397+
*[batched_symm_query[k] for k in ordered_symm_key]
398+
)
399+
)
400+
)
401+
381402
return super()._search( # type: ignore[return-value]
382403
num_chunks=num_chunks,
383404
chunk_size=chunk_size,

tests/client/test_core_client.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,21 @@ def test_warnings_exceptions():
9595

9696
with pytest.raises(MPRestError, match="Number of chunks must be greater than zero"):
9797
MaterialsRester()._get_all_documents({}, num_chunks=-1)
98+
99+
100+
def test_regression_batching():
101+
# See https://github.com/materialsproject/api/pull/1077
102+
# This test ensures that queries with batched input
103+
# that return no results do not infinitely recur
104+
105+
# This test should simply run if there is no regression
106+
107+
num_idxs = 100
108+
assert (
109+
len(
110+
MaterialsRester().search(
111+
material_ids=[f"mp-{idx}" for idx in range(num_idxs)], deprecated=True
112+
)
113+
)
114+
<= num_idxs
115+
)

tests/client/test_mprester.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,10 @@ def test_get_entries(self, mpr):
260260
def test_get_entries_in_chemsys(self, mpr):
261261
syms = ["Li", "Fe", "O"]
262262
syms2 = "Li-Fe-O"
263-
entries = mpr.get_entries_in_chemsys(syms)
263+
with pytest.warns(
264+
MPRestWarning, match="The default thermo type when retrieving entries"
265+
):
266+
entries = mpr.get_entries_in_chemsys(syms)
264267
entries2 = mpr.get_entries_in_chemsys(syms2)
265268
elements = {Element(sym) for sym in syms}
266269
for e in entries:
@@ -325,7 +328,9 @@ def test_get_pourbaix_entries(self, mpr):
325328
reason="`pip install 'mp-api[contribs]'` to use pourbaix functionality.",
326329
)
327330
def test_get_ion_entries(self, mpr):
328-
entries = mpr.get_entries_in_chemsys("Ti-O-H")
331+
entries = mpr.get_entries_in_chemsys(
332+
"Ti-O-H", additional_criteria={"thermo_types": ["GGA_GGA+U"]}
333+
)
329334
pd = PhaseDiagram(entries)
330335
ion_entry_data = mpr.get_ion_reference_data_for_chemsys("Ti-O-H")
331336
ion_entries = mpr.get_ion_entries(pd, ion_entry_data)
@@ -337,7 +342,9 @@ def test_get_ion_entries(self, mpr):
337342
assert len(bi_v_entry_data) == len(bi_data + v_data)
338343

339344
# test an incomplete phase diagram
340-
entries = mpr.get_entries_in_chemsys("Ti-O")
345+
entries = mpr.get_entries_in_chemsys(
346+
"Ti-O", additional_criteria={"thermo_types": ["GGA_GGA+U"]}
347+
)
341348
pd = PhaseDiagram(entries)
342349
with pytest.raises(ValueError, match="The phase diagram chemical system"):
343350
mpr.get_ion_entries(pd)
@@ -351,7 +358,8 @@ def test_get_ion_entries(self, mpr):
351358
itertools.chain.from_iterable(i.elements for i in ion_ref_comps)
352359
)
353360
ion_ref_entries = mpr.get_entries_in_chemsys(
354-
[*map(str, ion_ref_elts), "O", "H"]
361+
[*map(str, ion_ref_elts), "O", "H"],
362+
additional_criteria={"thermo_types": ["GGA_GGA+U"]},
355363
)
356364
mpc = MaterialsProjectAqueousCompatibility()
357365
ion_ref_entries = mpc.process_entries(ion_ref_entries)

0 commit comments

Comments
 (0)