Skip to content

Commit aff2373

Browse files
jan-janssenclaude
andauthored
docs: add explanatory markdown to example notebooks (#191)
The example notebooks under example_workflows/ had almost no prose, only code cells, which made the documentation hard to follow for new users. Add markdown cells explaining each workflow engine, what each step of the notebook does, and how the "Load Workflow with X" sections demonstrate interoperability, without changing any existing code cell. Also expand documentation/evcurve.md and documentation/nfdi.md (which were a few sentences each) to walk through the workflow.py functions and workflow.json structure the same way documentation/arithmetic.md already did, and lightly extend intro.md/conclusion.md. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 9e0fa80 commit aff2373

24 files changed

Lines changed: 902 additions & 210 deletions

‎documentation/conclusion.md‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
11
# Conclusion
22
Based on the Python Workflow Definition three rather different workflows were implemented in rather different workflow
3-
engines. This demonstrates the interoperability achieved with the Python Workflow Definition.
3+
engines: a simple arithmetic example coupling two Python functions, a Quantum Espresso energy volume curve calculation
4+
with fan-out parallelism, and a file based, multi-tool NFDI4Ing benchmark. In each case the same `workflow.json` was
5+
written by one workflow engine and successfully loaded and executed by the others, without any change to the underlying
6+
Python functions in `workflow.py`. This demonstrates the interoperability achieved with the Python Workflow Definition,
7+
and shows that it scales from small, in-memory Python workflows to larger, file based, multi-environment scientific
8+
workflows.
9+
10+
## Where to go next
11+
* Browse the [example_workflows](https://github.com/pythonworkflow/python-workflow-definition/tree/main/example_workflows)
12+
directory for the full set of notebooks, including engines not covered in this book such as `pyiron_workflow`,
13+
`executorlib` and CWL.
14+
* See the [README](https://github.com/pythonworkflow/python-workflow-definition) for installation instructions via
15+
`pip` or `conda`.
16+
* Read the accompanying publication: [J. Janssen et al., A python workflow definition for computational materials
17+
design, Digital Discovery, 2025](https://doi.org/10.1039/D5DD00231A).

‎documentation/evcurve.md‎

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,67 @@
11
# Energy Volume Curve
22
Based on [previous work](https://materialdigital.github.io/ADIS2023/README.html) from the [ADIS 2023 workshop](https://www.mpie.de/4902385/adis2023)
33
the calculation of an energy volume curve with the [quantum espresso](https://www.quantum-espresso.org) density
4-
functional theory (DFT) simulation code is implemented in the Python Workflow Definition.
4+
functional theory (DFT) simulation code is implemented in the Python Workflow Definition.
5+
6+
## Workflow
7+
An energy-volume curve is computed by relaxing a bulk crystal structure, straining it to a series of volumes and
8+
computing the total energy at each volume with a self-consistent-field (SCF) calculation. The pipeline is implemented
9+
as five Python functions in [workflow.py](example_workflows/quantum_espresso/workflow.py):
10+
```python
11+
def get_bulk_structure(element, a, cubic):
12+
# build an ASE bulk crystal structure, e.g. Al
13+
14+
def calculate_qe(working_directory, input_dict):
15+
# write a pw.x input file, run "pw.x -in input.pwi > output.pwo" and parse the pwscf.xml output;
16+
# input_dict["calculation"] selects "vc-relax" (cell + geometry relaxation) or "scf" (single-point energy)
17+
18+
def generate_structures(structure, strain_lst):
19+
# apply each volumetric strain in strain_lst to a structure, returning one structure per strain
20+
21+
def plot_energy_volume_curve(volume_lst, energy_lst):
22+
# plot energy against volume and save it as evcurve.png
23+
```
24+
`write_input`/`collect_output` handle the file-based `pw.x` input/output (`.pwi`/`.pwo`/`.xml` files), and
25+
`ase_to_json`/`json_to_ase` (de)serialize ASE `Atoms` objects to/from OPTIMADE-JSON strings, so structures can be
26+
passed between functions as plain JSON data instead of Python objects.
27+
28+
The workflow combines these functions as: `get_bulk_structure` -> `calculate_qe` (`vc-relax`, once) ->
29+
`generate_structures` -> `calculate_qe` (`scf`, once per strained structure) -> `plot_energy_volume_curve`. With five
30+
strains this means one relaxation followed by five independent SCF calculations that fan out from
31+
`generate_structures` and feed back into a single plot.
32+
33+
## workflow.json
34+
[workflow.json](example_workflows/quantum_espresso/workflow.json) encodes this fan-out as five separate
35+
`workflow.calculate_qe` function nodes, each connected to its own strained structure and its own `working_directory`
36+
input, all reading the same shared inputs (`pseudopotentials`, `kpts`, `calculation`, `smearing`) via
37+
`python_workflow_definition.shared.get_dict`. An excerpt showing the pattern for two of the five strained
38+
calculations:
39+
```
40+
{
41+
"nodes": [
42+
{"id": 2, "type": "function", "value": "workflow.generate_structures"},
43+
{"id": 3, "type": "function", "value": "workflow.calculate_qe"},
44+
{"id": 4, "type": "function", "value": "workflow.calculate_qe"},
45+
{"id": 19, "type": "input", "value": "strain_0", "name": "working_directory_1"},
46+
{"id": 20, "type": "function", "value": "python_workflow_definition.shared.get_dict"},
47+
{"id": 22, "type": "input", "value": "strain_1", "name": "working_directory_2"},
48+
{"id": 23, "type": "function", "value": "python_workflow_definition.shared.get_dict"}
49+
],
50+
"edges": [
51+
{"target": 3, "targetPort": "working_directory", "source": 19, "sourcePort": null},
52+
{"target": 20, "targetPort": "structure", "source": 2, "sourcePort": "s_0"},
53+
{"target": 3, "targetPort": "input_dict", "source": 20, "sourcePort": null},
54+
{"target": 4, "targetPort": "working_directory", "source": 22, "sourcePort": null},
55+
{"target": 23, "targetPort": "structure", "source": 2, "sourcePort": "s_1"},
56+
{"target": 4, "targetPort": "input_dict", "source": 23, "sourcePort": null}
57+
]
58+
}
59+
```
60+
Node 2 (`generate_structures`) exposes one output port per strain (`s_0`, `s_1`, ...); each port is wired into its
61+
own `get_dict`/`calculate_qe` pair. The energy and volume outputs of all five `calculate_qe` nodes are collected back
62+
into lists with `python_workflow_definition.shared.get_list` before being passed to `plot_energy_volume_curve`.
63+
64+
## Requirements
65+
Running this workflow (rather than just loading/inspecting the JSON) requires the Quantum Espresso `pw.x` binary,
66+
matching pseudopotential files (see [espresso/pseudo](example_workflows/quantum_espresso/espresso/pseudo)) and the
67+
Python dependencies listed in [environment.yml](example_workflows/quantum_espresso/environment.yml).

‎documentation/intro.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ Currently supported workflow engines:
1313
* [aiida-workgraph](https://github.com/aiidateam/aiida-workgraph)
1414
* [jobflow](https://github.com/materialsproject/jobflow)
1515
* [pyiron_base](https://github.com/pyiron/pyiron_base)
16+
* [pyiron_workflow](https://github.com/pyiron/pyiron_workflow)
17+
* [executorlib](https://github.com/pyiron/executorlib)
18+
* [CWL](https://www.commonwl.org/) via [cwltool](https://github.com/common-workflow-language/cwltool)
1619

1720
## Example Workflows
1821
Three workflows are implemented:

‎documentation/nfdi.md‎

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,61 @@
11
# NFDI4Ing Benchmark
2-
To demonstrate the compatibility of the Python Workflow Definition to file based workflows, the workflow benchmark
3-
developed as part of [NFDI4Ing](https://www.inggrid.org/article/id/3726/) is implemented for all three simulation codes
4-
based on the Python Workflow Definition.
2+
To demonstrate the compatibility of the Python Workflow Definition to file based workflows, the workflow benchmark
3+
developed as part of [NFDI4Ing](https://www.inggrid.org/article/id/3726/) is implemented for all workflow engines
4+
based on the Python Workflow Definition.
5+
6+
## Workflow
7+
Unlike the [arithmetic](arithmetic.md) and energy-volume-curve examples, which pass Python objects between functions,
8+
this benchmark chains together external command line tools that each require their own conda environment (defined in
9+
`source/envs/*.yaml`) and communicate through files rather than in-memory values. The
10+
pipeline is defined by six Python functions in [workflow.py](example_workflows/nfdi/workflow.py), each of which
11+
copies its inputs into a stage-specific subdirectory (`preprocessing`, `processing` or `postprocessing`) and calls
12+
its external tool with `conda_subprocess.check_output`:
13+
```python
14+
def generate_mesh(domain_size: float, source_directory: str) -> str: ...
15+
def convert_to_xdmf(gmsh_output_file: str) -> dict: ...
16+
def poisson(meshio_output_xdmf: str, meshio_output_h5: str, source_directory: str) -> dict: ...
17+
def plot_over_line(poisson_output_pvd_file: str, poisson_output_vtu_file: str, source_directory: str) -> str: ...
18+
def substitute_macros(pvbatch_output_file: str, ndofs: int, domain_size: float, source_directory: str) -> str: ...
19+
def compile_paper(macros_tex: str, plot_file: str, source_directory: str) -> str: ...
20+
```
21+
* `generate_mesh` runs [`gmsh`](https://gmsh.info) on `unit_square.geo` to mesh a 2D square whose side length is set
22+
by the `domain_size` parameter, producing a `.msh` file.
23+
* `convert_to_xdmf` runs `meshio convert` to turn the gmsh mesh into an XDMF/H5 file pair that the FEM solver can read.
24+
* `poisson` runs a [FEniCS](https://fenicsproject.org)-based `poisson.py` script that solves the Poisson equation on
25+
the mesh, returning the number of degrees of freedom (`numdofs`) alongside the `.pvd`/`.vtu` result files.
26+
* `plot_over_line` runs ParaView's `pvbatch` with `postprocessing.py` to sample the FEM solution along a line and
27+
write it out as a CSV file.
28+
* `substitute_macros` runs `prepare_paper_macros.py` to fill a LaTeX macro template with the plot data path, the
29+
domain size and the number of degrees of freedom.
30+
* `compile_paper` runs [`tectonic`](https://tectonic-typesetting.github.io) to compile `paper.tex`, which references
31+
those macros and the plot, into `paper.pdf`.
32+
33+
The connection of these Python functions is stored in the [workflow.json](example_workflows/nfdi/workflow.json)
34+
JSON file, following the same `nodes`/`edges` structure as the other examples. Each function is a `function` node,
35+
`domain_size` and `source_directory` are `input` nodes, and edges connect a function's input port either to another
36+
function's output port or directly to an input node:
37+
```
38+
{
39+
"version": "0.1.0",
40+
"nodes": [
41+
{"id": 0, "type": "function", "value": "workflow.generate_mesh"},
42+
{"id": 1, "type": "function", "value": "workflow.convert_to_xdmf"},
43+
{"id": 6, "type": "input", "value": 2.0, "name": "domain_size"},
44+
{"id": 7, "type": "input", "value": "source", "name": "source_directory"},
45+
{"id": 8, "type": "output", "name": "result"}
46+
],
47+
"edges": [
48+
{"target": 0, "targetPort": "domain_size", "source": 6, "sourcePort": null},
49+
{"target": 0, "targetPort": "source_directory", "source": 7, "sourcePort": null},
50+
{"target": 1, "targetPort": "gmsh_output_file", "source": 0, "sourcePort": null}
51+
]
52+
}
53+
```
54+
Since `convert_to_xdmf` and `poisson` return a `dict` instead of a single value, the edges that read their output
55+
set `sourcePort` to the specific dictionary key (e.g. `"xdmf_file"` or `"numdofs"`) rather than `null`.
56+
57+
Because every stage shells out to a different external tool in a different conda environment and passes file paths
58+
along the graph, this benchmark exercises a different part of the Python Workflow Definition than the arithmetic and
59+
energy-volume-curve examples: it shows that the same `workflow.json` produced by one engine can be handed to another
60+
engine, an execution manager like [CWL](https://www.commonwl.org)/`cwltool`, or plain Python, and still reproduce the
61+
same chain of `gmsh`, `meshio`, FEniCS, `pvbatch` and `tectonic` calls end to end.

‎example_workflows/arithmetic/aiida.ipynb‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@
2323
"cells": [
2424
{
2525
"cell_type": "markdown",
26-
"source": "# Aiida",
26+
"source": "# Aiida\n\nThis notebook defines the arithmetic workflow with [`aiida-workgraph`](https://github.com/aiidateam/aiida-workgraph) and then loads the resulting `workflow.json` into `jobflow`, `pyiron_base` and `pyiron_workflow`, to demonstrate that the same workflow definition can be executed by several different engines.",
2727
"metadata": {}
2828
},
2929
{
3030
"cell_type": "markdown",
31-
"source": "## Define workflow with aiida",
31+
"source": "## Define workflow with aiida\n\n`aiida-workgraph` represents a workflow as a `WorkGraph` of `Task`s. Tasks are executed and stored by the AiiDA engine, so we first need to connect to an AiiDA profile with `load_profile()` before any task can run.",
3232
"metadata": {}
3333
},
3434
{
@@ -63,6 +63,11 @@
6363
"outputs": [],
6464
"execution_count": 2
6565
},
66+
{
67+
"cell_type": "markdown",
68+
"source": "The arithmetic functions are imported under a leading underscore because they are still plain Python functions at this point; `wg.add_task` below turns each one into an AiiDA `Task` that can be linked to other tasks' inputs and outputs. `get_prod_and_div` returns a dictionary, so it is wrapped with `task(outputs=['prod', 'div'])` to expose `prod` and `div` as separate output sockets.",
69+
"metadata": {}
70+
},
6671
{
6772
"cell_type": "code",
6873
"source": "wg = WorkGraph(\"arithmetic\")",
@@ -99,6 +104,11 @@
99104
"outputs": [],
100105
"execution_count": 6
101106
},
107+
{
108+
"cell_type": "markdown",
109+
"source": "With all tasks connected, `write_workflow_json` walks the `WorkGraph`'s tasks and links and serializes them into the PWD `workflow.json` format: each task becomes a `function` node and each link becomes an edge between an input and an output port.",
110+
"metadata": {}
111+
},
102112
{
103113
"cell_type": "code",
104114
"source": "write_workflow_json(wg=wg, file_name=workflow_json_filename)",
@@ -125,7 +135,7 @@
125135
},
126136
{
127137
"cell_type": "markdown",
128-
"source": "## Load Workflow with jobflow",
138+
"source": "## Load Workflow with jobflow\n\nThe same `workflow.json` can now be loaded by a different engine. `python_workflow_definition.jobflow.load_workflow_json` reconstructs a jobflow `Flow` from the JSON, which is then executed locally with `run_locally`.",
129139
"metadata": {}
130140
},
131141
{
@@ -180,7 +190,7 @@
180190
},
181191
{
182192
"cell_type": "markdown",
183-
"source": "## Load Workflow with pyiron_base",
193+
"source": "## Load Workflow with pyiron_base\n\nThe same JSON file is loaded into `pyiron_base`, producing a list of delayed jobs. `.draw()` visualizes the dependency graph and `.pull()` triggers delayed execution, running each job in turn and returning the final result.",
184194
"metadata": {}
185195
},
186196
{
@@ -236,7 +246,7 @@
236246
{
237247
"metadata": {},
238248
"cell_type": "markdown",
239-
"source": "## Load Workflow with pyiron_workflow"
249+
"source": "## Load Workflow with pyiron_workflow\n\nFinally, the workflow is loaded into `pyiron_workflow`, producing a `Workflow` object whose node graph can be visualized with `.draw()` and executed with `.run()`."
240250
},
241251
{
242252
"metadata": {},
@@ -267,4 +277,4 @@
267277
"source": "wf.run()"
268278
}
269279
]
270-
}
280+
}

0 commit comments

Comments
 (0)