Skip to content

Commit aa0c8b3

Browse files
vineetbansalAndrew-S-Rosenpre-commit-ci[bot]
authored
Delegating quacc @flow to jobflow @flow (#3016)
## Summary of Changes \>> Provide context and a description of your changes here. Make sure to reference any associated issues. << ### Requirements - [x] My PR is focused on a [single feature addition or bugfix](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/getting-started/best-practices-for-pull-requests#write-small-prs). - [x] My PR has relevant, comprehensive [unit tests](https://quantum-accelerators.github.io/quacc/dev/contributing.html#unit-tests). - [x] My PR is on a [custom branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-and-deleting-branches-within-your-repository) (i.e. is _not_ named `main`). Note: If you are an external contributor, you will see a comment from [@buildbot-princeton](https://github.com/buildbot-princeton). This is solely for the maintainers. --------- Co-authored-by: Andrew S. Rosen <asrosen93@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 76fb8d0 commit aa0c8b3

File tree

11 files changed

+267
-43
lines changed

11 files changed

+267
-43
lines changed

docs/user/basics/wflow_decorators.md

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,20 +80,16 @@ A `#!Python @subflow` in quacc is any workflow that returns a list of job output
8080

8181
=== "Jobflow"
8282

83-
Take a moment to read the Jobflow documentation's [Quick Start](https://materialsproject.github.io/jobflow/tutorials/1-quickstart.html) to get a sense of how Jobflow works. Namely, you should understand the `Job` and `Flow` definitions, which describe individual compute tasks and workflows, respectively.
83+
Take a moment to read the Jobflow documentation's [Quick Start](https://materialsproject.github.io/jobflow/tutorials/1-quickstart.html) to get a sense of how Jobflow works. Namely, you should understand the concept of a `#!Python @job` and a `#!Python @flow`, which describe individual compute tasks and workflows, respectively.
8484

8585
<center>
8686

87-
| Quacc | Jobflow |
88-
| ------------------- | --------------- |
89-
| `#!Python @job` | `#!Python @job` |
90-
| `#!Python @flow` | N/A |
91-
| `#!Python @subflow` | N/A |
87+
| Quacc | Jobflow |
88+
| ------------------- | ---------------- |
89+
| `#!Python @job` | `#!Python @job` |
90+
| `#!Python @flow` | `#!Python @flow` |
91+
| `#!Python @subflow` | `#!Python @job` |
9292

9393
</center>
9494

95-
!!! Warning
96-
97-
Due to the difference in how Jobflow handles workflows compared to other supported workflow engines, any quacc recipes that have been pre-defined with a `#!Python @flow` or `#!Python @subflow` decorator (i.e. have `_flow` in the name) cannot be run directly with Jobflow.
98-
9995
The quacc descriptors are drop-in replacements for the specified workflow engine analogue, which we will use for the remainder of the tutorials. Based on the value for the `WORKFLOW_ENGINE` global variable in your [quacc settings](../settings/settings.md), the appropriate decorator will be automatically selected. If the `WORKFLOW_ENGINE` setting is set to `None` (i.e. `quacc set WORKFLOW_ENGINE None`), the decorators will have no effect on the underlying function.

docs/user/basics/wflow_overview.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,6 @@ Everyone's computing needs are different, so we ensured that quacc is interopera
8787

8888
[Jobflow](https://github.com/materialsproject/jobflow) is developed and maintained by the Materials Project team at Lawrence Berkeley National Laboratory and serves as a seamless interface to [FireWorks](https://github.com/materialsproject/fireworks) or [Jobflow Remote](https://github.com/Matgenix/jobflow-remote) for dispatching and monitoring compute jobs.
8989

90-
!!! Warning
91-
92-
Jobflow is not yet compatible with the `#!Python @flow` or `#!Python @subflow` decorators used in many quacc recipes and so should only be used if necessary. See [this issue](https://github.com/Quantum-Accelerators/quacc/issues/1061) to track the progress of this enhancement.
93-
9490
Pros:
9591

9692
- Native support for a variety of databases

docs/user/wflow_engine/wflow_engines1.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,20 @@ graph LR
266266

267267
=== "Jobflow"
268268

269-
!!! Warning
269+
```python
270+
import jobflow as jf
271+
from ase.build import bulk
272+
from quacc.recipes.emt.slabs import bulk_to_slabs_flow
273+
274+
# Define the Atoms object
275+
atoms = bulk("Cu")
270276

271-
Due to the difference in how Jobflow handles workflows (particularly dynamic ones) compared to other supported workflow engines, any quacc recipes that have been pre-defined with a `#!Python @flow` decorator (i.e. have `_flow` in the name) cannot be run directly with Jobflow. Rather, a Jobflow-specific `Flow` needs to be constructed by the user.
277+
# Create the workflow with arguments
278+
workflow = bulk_to_slabs_flow(atoms)
279+
280+
# Dispatch the workflow and get results
281+
results = jf.run_locally(workflow)
282+
283+
# Print the results
284+
print(results)
285+
```

docs/user/wflow_engine/wflow_engines2.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,32 @@ graph LR
641641

642642
=== "Jobflow"
643643

644-
!!! Warning "Limitations"
644+
```python
645+
import jobflow as jf
646+
from ase.build import bulk
647+
from quacc import flow
648+
from quacc.recipes.emt.core import relax_job
649+
from quacc.recipes.emt.slabs import bulk_to_slabs_flow
650+
651+
652+
# Define the workflow
653+
@flow
654+
def relaxed_slabs_workflow(atoms):
655+
relaxed_bulk = relax_job(atoms)
656+
relaxed_slabs = bulk_to_slabs_flow(relaxed_bulk["atoms"], run_static=False)
645657

646-
Due to the difference in how Jobflow handles workflows (particularly dynamic ones) compared to other supported workflow engines, any quacc recipes that have been pre-defined with a `#!Python @flow` decorator (i.e. have `_flow` in the name) cannot be run directly with Jobflow. Rather, a Jobflow-specific `Flow` needs to be constructed by the user.
658+
return relaxed_slabs
659+
660+
661+
# Define the Atoms object
662+
atoms = bulk("Cu")
663+
664+
# Create the workflow with arguments
665+
workflow = relaxed_slabs_workflow(atoms)
666+
667+
# Dispatch the workflow and get results
668+
results = jf.run_locally(workflow)
669+
670+
# print the results
671+
print(results)
672+
```

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ dependencies = [
4444
dask = ["dask[distributed]>=2023.12.1", "dask-jobqueue>=0.8.2"]
4545
defects = ["pymatgen-analysis-defects>=2024.10.22", "shakenbreak>=3.2.0"]
4646
fairchem = ["fairchem-data-omat>=0.2", "fairchem-data-oc>=1.0.2", "fairchem-core>=2.2.0"]
47-
jobflow = ["jobflow>=0.1.14", "jobflow-remote>=1.0.0"]
47+
jobflow = ["jobflow>=0.3.0", "jobflow-remote>=1.0.0"]
4848
orb = ["orb-models>=0.4.1"]
4949
mace = ["mace-torch>=0.3.3", "mace-models>=0.1.6"]
5050
matgl = ["matgl>=2.0.2"]

src/quacc/wflow_tools/customizers.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,17 @@ def update_parameters(
133133
func = strip_decorator(func)
134134
return decorator_func(partial(func, **params))
135135

136-
return partial(func, **params)
136+
partial_fn = partial(func, **params)
137+
# Assigning a __name__ allows monty's jsanitize function to work correctly
138+
# with this partial function.
139+
if hasattr(func, "name"):
140+
partial_fn.__name__ = func.name
141+
elif hasattr(func, "__name__"):
142+
partial_fn.__name__ = func.__name__
143+
else:
144+
partial_fn.__name__ = ""
145+
146+
return partial_fn
137147

138148

139149
def customize_funcs(

src/quacc/wflow_tools/decorators.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,7 @@ def wrapper(*f_args, **f_kwargs):
145145

146146
return Delayed_(delayed(wrapper, **kwargs))
147147
elif settings.WORKFLOW_ENGINE == "jobflow":
148-
from jobflow import job as jf_job
149-
150-
return jf_job(_func, **kwargs)
148+
return _get_jobflow_wrapped_func(_func, **kwargs)
151149
elif settings.WORKFLOW_ENGINE == "parsl":
152150
from parsl import python_app
153151

@@ -311,6 +309,8 @@ def workflow(a, b, c):
311309
return task(_func, namespace=_func.__module__, **kwargs)
312310
elif settings.WORKFLOW_ENGINE == "prefect":
313311
return _get_prefect_wrapped_flow(_func, settings, **kwargs)
312+
elif settings.WORKFLOW_ENGINE == "jobflow":
313+
return _get_jobflow_wrapped_flow(_func)
314314
else:
315315
return _func
316316

@@ -507,6 +507,8 @@ def wrapper(*f_args, **f_kwargs):
507507
from redun import task
508508

509509
return task(_func, namespace=_func.__module__, **kwargs)
510+
elif settings.WORKFLOW_ENGINE == "jobflow":
511+
return _get_jobflow_wrapped_func(_func, **kwargs)
510512
else:
511513
return _func
512514

@@ -584,6 +586,18 @@ def sync_wrapper(*f_args, **f_kwargs):
584586
return prefect_flow(_func, validate_parameters=False, **kwargs)
585587

586588

589+
def _get_jobflow_wrapped_func(method=None, **job_kwargs):
590+
from jobflow import job as jf_job
591+
592+
return jf_job(method, **job_kwargs)
593+
594+
595+
def _get_jobflow_wrapped_flow(_func: Callable) -> Callable:
596+
from jobflow import flow as jf_flow
597+
598+
return jf_flow(_func)
599+
600+
587601
class Delayed_:
588602
"""A small Dask-compatible, serializable object to wrap delayed functions that we
589603
don't want to execute.

tests/jobflow/test_copy_files.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from pathlib import Path
88

9-
from quacc import job
9+
from quacc import flow, job
1010
from quacc.wflow_tools.job_argument import Copy
1111

1212

@@ -24,11 +24,15 @@ def create_file(name: str, copy: Copy | None = None):
2424

2525
return {"dir_name": output_dir}
2626

27-
job1 = create_file("job1")
28-
job2 = create_file("job2", copy=Copy({job1.output["dir_name"]: "job1*"}))
29-
flow = jf.Flow([job1, job2])
30-
assert str(flow.graph) == "DiGraph with 2 nodes and 1 edges"
31-
jf.run_locally(flow, ensure_success=True, create_folders=True)
27+
@flow
28+
def create_files():
29+
job1 = create_file("job1")
30+
job2 = create_file("job2", copy=Copy({job1["dir_name"]: "job1*"}))
31+
return [job1, job2]
32+
33+
workflow = create_files()
34+
assert str(workflow.graph) == "DiGraph with 2 nodes and 1 edges"
35+
jf.run_locally(workflow, ensure_success=True, create_folders=True)
3236

3337
# Individual job folders/files should exist, and the job1 file should be
3438
# copied over to the job2 folder.

tests/jobflow/test_emt_recipes.py

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,36 @@
77

88
from ase.build import bulk
99

10+
from quacc import flow, job
1011
from quacc.recipes.emt.core import relax_job
11-
from quacc.wflow_tools.job_argument import Copy
12+
from quacc.recipes.emt.slabs import bulk_to_slabs_flow
13+
14+
15+
@pytest.mark.parametrize("job_decorators", [None, {"relax_job": job()}])
16+
def test_functools(tmp_path, monkeypatch, job_decorators):
17+
monkeypatch.chdir(tmp_path)
18+
atoms = bulk("Cu")
19+
flow = bulk_to_slabs_flow(
20+
atoms,
21+
run_static=False,
22+
job_params={"relax_job": {"opt_params": {"fmax": 0.1}}},
23+
job_decorators=job_decorators,
24+
)
25+
jf.run_locally(flow, ensure_success=True)
26+
27+
28+
def test_copy_files(tmp_path, monkeypatch):
29+
monkeypatch.chdir(tmp_path)
30+
atoms = bulk("Cu")
31+
32+
@flow
33+
def myflow(atoms):
34+
result1 = relax_job(atoms)
35+
return relax_job(result1["atoms"], copy_files={result1["dir_name"]: "opt.*"})
36+
37+
output = jf.run_locally(myflow(atoms))
38+
first_output = next(iter(output.values()))[1].output
39+
assert "atoms" in first_output
1240

1341

1442
def test_folders(tmp_path, monkeypatch):
@@ -23,13 +51,25 @@ def test_folders(tmp_path, monkeypatch):
2351
assert "opt.log.gz" in os.listdir(tmp_path / files[0])
2452

2553

26-
def test_copy_files(tmp_path, monkeypatch):
54+
def test_relax_flow(tmp_path, monkeypatch):
2755
monkeypatch.chdir(tmp_path)
2856
atoms = bulk("Cu")
2957

30-
job1 = relax_job(atoms)
31-
job2 = relax_job(
32-
job1.output["atoms"], copy_files=Copy({job1.output["dir_name"]: "opt.*"})
33-
)
34-
flow = jf.Flow([job1, job2])
35-
jf.run_locally(flow, ensure_success=True, create_folders=True)
58+
@flow
59+
def relax_flow(atoms):
60+
result1 = relax_job(atoms)
61+
return relax_job(result1["atoms"])
62+
63+
jf.run_locally(relax_flow(atoms), ensure_success=True)
64+
65+
66+
def test_relaxed_slabs(tmp_path, monkeypatch):
67+
monkeypatch.chdir(tmp_path)
68+
atoms = bulk("Cu")
69+
70+
@flow
71+
def workflow(atoms):
72+
relaxed_bulk = relax_job(atoms)
73+
return bulk_to_slabs_flow(relaxed_bulk["atoms"], run_static=False)
74+
75+
jf.run_locally(workflow(atoms), ensure_success=True)

tests/jobflow/test_syntax.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ def workflow(a, b, c):
3232
assert hasattr(mult, "original")
3333
assert isinstance(add(1, 2), jf.Job)
3434
assert isinstance(mult(1, 2), jf.Job)
35-
assert isinstance(workflow(1, 2, 3), jf.Job)
36-
assert isinstance(add_distributed([1, 2, 3], 4)[0], jf.Job)
35+
assert isinstance(workflow(1, 2, 3), jf.Flow)
36+
assert isinstance(add_distributed([1, 2, 3], 4), jf.Job)
3737

3838

3939
def test_jobflow_decorators_args(tmp_path, monkeypatch):
@@ -61,5 +61,5 @@ def workflow(a, b, c):
6161
assert hasattr(mult, "original")
6262
assert isinstance(add(1, 2), jf.Job)
6363
assert isinstance(mult(1, 2), jf.Job)
64-
assert isinstance(workflow(1, 2, 3), jf.Job)
65-
assert isinstance(add_distributed([1, 2, 3], 4)[0], jf.Job)
64+
assert isinstance(workflow(1, 2, 3), jf.Flow)
65+
assert isinstance(add_distributed([1, 2, 3], 4), jf.Job)

0 commit comments

Comments
 (0)