Skip to content

Conversation

@tsmathis
Copy link
Collaborator

should just work™️

@tsmathis
Copy link
Collaborator Author

tsmathis commented Oct 23, 2025

some notes:

1. depends on #1021
2. could supersede #974
3. this will need to be addressed for the progress bar to be accurate in the case were has_gnome_access=False:

# TODO: Update tasks (+ others?) resource to have emmet-api BatchIdQuery operator
# -> need to modify BatchIdQuery operator to handle root level
# batch_id, not only builder_meta.batch_id
# if not has_gnome_access:
# num_docs_needed = self.count(
# {"batch_id_neq_any": SETTINGS.ACCESS_CONTROLLED_BATCH_IDS}
# )

the count can be retrieved from s3, but the COUNT(*) ... WHERE NOT IN ... is slow
4. wasn't sure how to emit messages to the user, warnings might not be the best choice:
warnings.warn(
f"Dataset for {suffix} already exists at {target_path}, delete or move existing dataset "
"or re-run search query with MPRester(force_renew=True)",
MPLocalDatasetWarning,

warnings.warn(
f"Dataset for {suffix} written to {target_path}. It is recommended to optimize "
"the table according to your usage patterns prior to running intensive workloads, "
"see: https://delta-io.github.io/delta-rs/delta-lake-best-practices/#optimizing-table-layout",
MPLocalDatasetWarning,
)

5. On the fence if MPDataset should inherit user's choice of use_document_model or default to False, its extra overhead when True
use_document_model=self.use_document_model,

6. re: document model's, wasn't sure if making an MPDataDoc model was the right route so the emmet model is just passed through now.
7. @esoteric-ephemera, is this how coercing user input to AlphaIDs should go? Do you want to do something different?
as_alpha = str(AlphaID(task_id, padlen=8)).split("-")[-1]

8. Is MPAPIClientSettings the right place for these? Not sure if the user has the ability to adjust these if needed:
LOCAL_DATASET_CACHE: str = Field(
os.path.expanduser("~") + "/mp_datasets",
description="Target directory for downloading full datasets",
)
DATASET_FLUSH_THRESHOLD: int = Field(
100000,
description="Threshold number of rows to accumulate in memory before flushing dataset to disk",
)
ACCESS_CONTROLLED_BATCH_IDS: list[str] = Field(
["gnome_r2scan_statics"], description="Batch ids with access restrictions"
)

@tsmathis
Copy link
Collaborator Author

ah and based on the failing test for trajectories, I assumed returning the pymatgen object was correct, should the dict be returned? @esoteric-ephemera

return RelaxTrajectory(**traj_data[0]).to_pmg()

@esoteric-ephemera
Copy link
Collaborator

esoteric-ephemera commented Oct 23, 2025

@tsmathis think the API was set up to return the jsanitized trajectory info:
https://github.com/materialsproject/emmet/blob/3447c5af4746d539f1f4faf26b97715cb119c85d/emmet-api/emmet/api/routes/materials/tasks/query_operators.py#L73

Either way yeah I guess it returned the as_dict but we don't need to keep with that paradigm

For the AlphaID, to handle either the no prefix/separator ("aaaaaaft") and with prefix/separator ("mp-aaaaaaft") cases, both of these should work, but I can also just save the "padded identifier" as an attr on it to make this cleaner - I'll do that in the PR you linked:

"a"*(x._padlen-len(x._identifier)) + x._identifier

or

if (alpha := AlphaID(task_id, padlen=8))._separator:
  padded = str(alpha).rsplit(alpha._separator)[-1] 
else:
  padded = str(alpha)

@tsmathis
Copy link
Collaborator Author

tsmathis commented Oct 23, 2025

For the AlphaID, to handle either the no prefix/separator ("aaaaaaft") and with prefix/separator ("mp-aaaaaaft") cases, both of these should work, but I can also just save the "padded identifier" as an attr on it to make this cleaner - I'll do that in the PR you linked:

either way on this works for me, just want to make sure I stick to the intended usage (edit: or that we're at least consistent across the client)

Either way yeah I guess it returned the as_dict but we don't need to keep with that paradigm

Was going to say we could stick to whatever the frontend was expecting, but looking now the frontend doesn't even use the tasks.get_trajectory(...) function so it will need to be rewritten either way. The frontend does end up making a dataframe from the trajectory dict, so maybe just returning the dict will be best

@codecov-commenter
Copy link

codecov-commenter commented Oct 23, 2025

Codecov Report

❌ Patch coverage is 42.10526% with 77 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.10%. Comparing base (52a3c57) to head (b2a832f).

Files with missing lines Patch % Lines
mp_api/client/core/client.py 26.38% 53 Missing ⚠️
mp_api/client/core/utils.py 47.82% 24 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1023      +/-   ##
==========================================
- Coverage   67.29%   66.10%   -1.20%     
==========================================
  Files          50       50              
  Lines        2770     2894     +124     
==========================================
+ Hits         1864     1913      +49     
- Misses        906      981      +75     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Member

@tschaume tschaume left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice! Looking forward to rolling this out 😄

Copy link
Member

@tschaume tschaume left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the updates @tsmathis ! Found a few more potential issues/improvements

@tsmathis
Copy link
Collaborator Author

tsmathis commented Nov 5, 2025

good catches @tschaume

you're obviously free to keep adding changes for testing, but you can also just ping me if you want me to update things as changes come in upstream

@tsmathis

This comment was marked as outdated.

)

LOCAL_DATASET_CACHE: str = Field(
os.path.expanduser("~") + "/mp_datasets",
Copy link
Collaborator

@esoteric-ephemera esoteric-ephemera Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may want to change to just os.path.expanduser("~/mp_datasets") so that os can resolve non-unix-like separators. Or just use pathlib.Path("~/mp_datasets").expanduser()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point
7ee5515

)

DATASET_FLUSH_THRESHOLD: int = Field(
100000,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make this a byte threshold in memory with pyarrow.Table.get_total_buffer_size? Would be an overestimate but that's probably safe for this case

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if that would work exactly since the in memory accumulator is a pylist of pa.RecordBatchs.

I'll look around for something that's more predictable for the flush threshold than just number of rows since row sizes can vary drastically across different data products.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some looking, RecordBatch also has get_total_buffer_size()

What do you think a good threshold would be in this case? For the first 100k rows for the tasks table I got 2770781904 bytes (2.7 GB)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The corresponding on disk size (compressed w/ zstd) for that first 100k rows is 422M

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2.5-2.75 GB spill is probably good

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@esoteric-ephemera
Copy link
Collaborator

Working great for me! Full task download in ~6 min which is crazy compared to before

General discussion quetion: Now that we're working with just bare AlphaID (e.g., aaaaaaft), we may need to manually insert prefixes by endpoint right? Once the various materials endpoints are delta_backed, they should just get mp-. Do we want to manually insert a mpt- prefix or task- for the tasks?

@tsmathis
Copy link
Collaborator Author

I think for the core tasks collection we're going prefix-less, correct? All the others will get prefixes at parse/build time.

@tsmathis
Copy link
Collaborator Author

tsmathis commented Nov 12, 2025

re: the iterating, indexing into the local dataset, etc

I am a little conflicted on what the best route for the python-like implementation/behavior of the local MPDatasets should be. Mainly because as soon as we leave arrow-land we're neutering the performance that can be achieved.

As an example:
Regardless of how we do the iteration behavior, this is dog water:

# doesn't work currently, would have to update iterating to match Aaron's review comment first
>>> tasks = mpr.materials.tasks.search()
>>> non_metallic_r2scan_structures = [
    x.structure 
    for x in tasks 
    if x.output.bandgap > 0 and x.run_type == "r2SCAN"
]

compared to:

>>> import pyarrow.compute as pc
>>> tasks_ds = tasks.pyarrow_dataset
>>> expr = (pc.field(("output", "bandgap")) > 0) & (pc.field("run_type") == "r2SCAN")
>>> non_metallic_r2scan_structures = tasks_ds.to_table(columns=["structure"], filter=expr)

which is sub-second execution on my machine

I am obviously biased on this front since I am comfortable with arrow's usage patterns, not sure if the average client user would be willing to go down that route. Ideally though we should be guiding users towards a "golden path".

@esoteric-ephemera
Copy link
Collaborator

Yeah it's hard to say what's best in this case. We'd probably want to prioritize user experience across endpoints, or just throw a specialized warning for full task retrieval that the return type is different

If pandas is a no-op from parquet (not sure if that's also true for the dataset or just an individual table/array) then that could be a viable alternative? Feel like pandas will be more familiar than arrow datasets

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants