-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #88 from pyiron/update_examples
Update examples
- Loading branch information
Showing
6 changed files
with
773 additions
and
107 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,92 @@ | ||
# Ironflow | ||
|
||
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/pyiron/ironflow/HEAD?labpath=ironflow.ipynb) | ||
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/pyiron/ironflow/HEAD?labpath=example.ipynb) | ||
[![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) | ||
|
||
Combines [ryven](https://ryven.org), [ipycanvas](https://ipycanvas.readthedocs.io/) and [pyiron](https://pyiron.org). | ||
Ironflow combines [ryven](https://ryven.org), [ipycanvas](https://ipycanvas.readthedocs.io/) and [ipywidgets](https://ipywidgets.readthedocs.io/en/stable/) to provide a Jupyter-based visual scripting gui for running [pyiron](https://pyiron.org) workflow graphs. | ||
This project is under active development, and in particular the set of nodes available for the workflow graphs is still limited. | ||
If there is a particular use-case you'd like to see, or if one of our nodes is not working as expected, please raise an issue! | ||
|
||
![](screenshot.png) | ||
|
||
## Usage | ||
|
||
The main gui can be imported directly from `ironflow`. | ||
|
||
The gui takes a session title at instantiation, and will automatically try to load any saved session (a JSON file) with the same name present. | ||
To visualize the gui, call the `draw` method. | ||
E.g.: | ||
```python | ||
from ironflow import GUI | ||
gui = GUI('example') | ||
gui.draw() | ||
``` | ||
|
||
In addition to manipulating the gui with buttons in the toolbar, you can: | ||
- Look at a node's IO values by clicking on it (which selects it) | ||
- Deselect things by clicking on empty space | ||
- See a richer representation of the node by clicking its `SHOW` button | ||
- Connect the IO (input/output) of a node by clicking on its port and then clicking on another node's OI port | ||
- Move a node around by clicking and dragging it | ||
- Pan the entire camera around by clicking and dragging empty space | ||
- Add a new node of the selected type by double-clicking on empty space | ||
- Delete a node by double-clicking on it | ||
- Collapse or expand a node by clicking on the little triangle on its body (has no effect on functionality, just makes it take less space) | ||
|
||
In the default `data` execution mode (we don't currently do anything with the `exec` mode, so don't worry about it), nodes will update their output whenever their input data changes. | ||
You'll see the node body change color when it's performing this update. | ||
Some nodes have input (or output) ports that are of the execution rather than data type. | ||
These can be triggered by a signal from another node's exec-type output port, or by manually clicking the button associated with that port right there in the node widget. | ||
|
||
### Adding custom nodes | ||
|
||
The tools needed for extending your graphs with new custom nodes can be imported from `ironflow.custom_nodes`. | ||
New nodes can be registered either from a list of nodes, or from a python module or .py file. | ||
In the latter two cases, only those nodes that inherit from `Node` *and* have a class name ending in `_Node` will be registered (this allows you to have your own node class templates and avoid loading the template itself by simply using regular python CamelCase naming conventions and avoiding ending in `_Node`). | ||
|
||
A new node should have a `title` and may optionally have input and/or output channels specified. | ||
If you want your node to actually *do* something, you'll also need to define an `update_event` method. | ||
E.g.: | ||
|
||
```python | ||
from ironflow.custom_nodes import Node, NodeInputBP, NodeOutputBP, dtypes, input_widgets | ||
|
||
class My_Node(Node): | ||
title = "MyUserNode" | ||
init_inputs = [ | ||
NodeInputBP(dtype=dtypes.Integer(default=1), label="foo") | ||
] | ||
init_outputs = [ | ||
NodeOutputBP(label="bar") | ||
] | ||
color = 'cyan' | ||
|
||
def update_event(self, inp=-1): | ||
self.set_output_val(0, self.input(0) + 42) | ||
|
||
gui.register_node(My_Node) | ||
``` | ||
|
||
Ironflow nodes differ from standard ryven nodes in three ways: | ||
- There is a new helper method `output` analogous to the existing `input` method that lets you more easily access output values, i.e. just a quality-of-life difference. | ||
- They have a `representation` dictionary, which is used by the IPython gui front-end to give a richer look at nodes -- but this just defaults to all the outputs, so you don't need to touch it if you don't want to. | ||
- They have two new events: `before_update` and `after_update`, to which you can connect (e.g. `node.after_update.connect`) or disconnect (`...disconnect`) methods to fire before and/or after updates occur -- such methods must take the node instance itself as the first argument, and the canonical input integer (specifying which input value it is that's updating) as the second argument. (You can see an example of this in our base `Node` class, where we use it to force an update of the `representation` attribute after each node update.) | ||
|
||
Otherwise, they are just standard ryven nodes, and all the ryven documentation applies. | ||
|
||
## Structure | ||
|
||
The code is broken into three main submodules: | ||
- `model`, which provides and interface to and extensions of the ryven back-end | ||
- `gui`, which has all the code for driving the back-end from the IPython visual interface | ||
- `nodes`, which stores all the nodes that get included by default when you instantiate the gui/model | ||
|
||
There is also a `custom_nodes` submodule, but this just exposes other parts of the code base in one easy-to-improt-from spot. | ||
|
||
The model itself, `HasSession`, is just a driver for a single ryven `Session`, with some helpful tools like the ability to easily register new nodes. | ||
The only ryven element we currently extend is the `Node` class, as discussed above; other components are just imported directly from `ryvencore` in `ironflow.model.__init__`. | ||
|
||
The gui inherits from a drives the model. | ||
The visual elements of the gui are broken down into subcomponents like the toolbar, a panel with a visual representaiton of the graph, a place to show the node representations, etc. | ||
We avoid listing them all here because what's included and how it's laid out is still in flux. | ||
The key conceptual bit is that these various sub-components do not rely directly on eachother's internal implementation, they go through the gui as an intermediary. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
{ | ||
"cells": [ | ||
{ | ||
"cell_type": "markdown", | ||
"id": "18e99e8d-4818-440e-9b47-1f0d80e18214", | ||
"metadata": {}, | ||
"source": [ | ||
"We can import the gui and instantiate a new session. If there is a saved session with the same name, it will be automatically loaded. To see the gui, use the `.draw()` method." | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 1, | ||
"id": "a0a60e2d", | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"%matplotlib inline\n", | ||
"from ironflow import GUI" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 2, | ||
"id": "e8da0476-4a99-4da3-8fe3-e12c646893e8", | ||
"metadata": {}, | ||
"outputs": [ | ||
{ | ||
"data": { | ||
"application/vnd.jupyter.widget-view+json": { | ||
"model_id": "9168bb0264ef413283bcf16576a20181", | ||
"version_major": 2, | ||
"version_minor": 0 | ||
}, | ||
"text/plain": [] | ||
}, | ||
"metadata": {}, | ||
"output_type": "display_data" | ||
}, | ||
{ | ||
"name": "stdout", | ||
"output_type": "stream", | ||
"text": [ | ||
"Loaded session data for example\n" | ||
] | ||
} | ||
], | ||
"source": [ | ||
"gui = GUI('example')" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 3, | ||
"id": "08ae0940-638e-4dde-a226-b15ea0a28230", | ||
"metadata": {}, | ||
"outputs": [ | ||
{ | ||
"data": { | ||
"application/vnd.jupyter.widget-view+json": { | ||
"model_id": "6ed3fcb66dde4bf28883a4372df5fa9b", | ||
"version_major": 2, | ||
"version_minor": 0 | ||
}, | ||
"text/plain": [ | ||
"VBox(children=(HBox(children=(Dropdown(layout=Layout(width='80px'), options=('data', 'exec'), value='data'), B…" | ||
] | ||
}, | ||
"metadata": {}, | ||
"output_type": "display_data" | ||
} | ||
], | ||
"source": [ | ||
"gui.draw()" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"id": "c1e40729-7e9d-42a4-be91-de1e311241fb", | ||
"metadata": {}, | ||
"source": [ | ||
"We can also extend ironflow with new nodes on-the-fly. Most of the tools you should need are stored under `ironflow.custom_nodes`. Once we register a new node from a notebook, it immediately shows up under the `__main__` tab in the node selector." | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 4, | ||
"id": "a9336fda-a194-4b59-a912-6019e000d579", | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"from ironflow.custom_nodes import Node, NodeInputBP, NodeOutputBP, dtypes, input_widgets\n", | ||
"\n", | ||
"class My_Node(Node):\n", | ||
" title = \"MyUserNode\"\n", | ||
" init_inputs = [\n", | ||
" NodeInputBP(dtype=dtypes.Integer(default=1), label=\"foo\")\n", | ||
" ]\n", | ||
" init_outputs = [\n", | ||
" NodeOutputBP(label=\"bar\")\n", | ||
" ]\n", | ||
" color = 'cyan'\n", | ||
"\n", | ||
" def update_event(self, inp=-1):\n", | ||
" self.set_output_val(0, self.input(0) + 42)\n", | ||
"\n", | ||
"gui.register_node(My_Node)" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"id": "f62462ef-a1ea-4e43-8c9f-a78c1b91a75e", | ||
"metadata": {}, | ||
"source": [ | ||
"If we save a session with a custom node, the same node needs to registered again *before* we load that session! To instantiate and load such a saved session all at once, extra node packages can be included using the optional `extra_node_packages` argument. This takes a `list` of node packages, which should either be a list of nodes that are children of `Node` (as in the example below) -- these appear under `__main__` in the gui, or a python module or path to a .py file. When registering nodes from a module or file, only those that inherit from `Node` *and* have a class name ending in `_Node` will be registered (this allows you to have your own node class templates and avoid loading the template itself by simply using regular python CamelCase naming conventions and avoiding ending in `_Node`). " | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 5, | ||
"id": "7f753105-1669-4163-a338-6b7887b820b2", | ||
"metadata": {}, | ||
"outputs": [ | ||
{ | ||
"name": "stdout", | ||
"output_type": "stream", | ||
"text": [ | ||
"No session data found for example_with_custom_node, making a new script.\n" | ||
] | ||
} | ||
], | ||
"source": [ | ||
"gui2 = GUI(\"example_with_custom_node\", extra_nodes_packages=[[My_Node]])" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 6, | ||
"id": "61bd0c78-a148-483a-b232-f1e51e2ef2d7", | ||
"metadata": {}, | ||
"outputs": [ | ||
{ | ||
"data": { | ||
"application/vnd.jupyter.widget-view+json": { | ||
"model_id": "4fa0213eeaa54243b6493c8f64e7dfdf", | ||
"version_major": 2, | ||
"version_minor": 0 | ||
}, | ||
"text/plain": [ | ||
"VBox(children=(HBox(children=(Dropdown(layout=Layout(width='80px'), options=('data', 'exec'), value='data'), B…" | ||
] | ||
}, | ||
"metadata": {}, | ||
"output_type": "display_data" | ||
} | ||
], | ||
"source": [ | ||
"gui2.draw()" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"id": "e194ae29-1c44-4f6b-a95d-305f48383c80", | ||
"metadata": {}, | ||
"source": [ | ||
"Note: When registering nodes from a module or file, they appear in the tab based on the end of the module/file path (excluding the .py convention). This is intentional since nodes from multiple sources may be conceptually linked, so they get grouped with every other node that has the same location terminus, but it's possible it could lead to naming conflicts. You're already able to override this with the underlying `GUI.register_nodes` method, which allows you to specify your own location using the optional `node_group` argument. In a future update we plan to provide the same capability when registering nodes at initialization." | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"id": "9601919f-9011-4ecc-9b93-4c1a6ed45ef7", | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [] | ||
} | ||
], | ||
"metadata": { | ||
"kernelspec": { | ||
"display_name": "Python 3", | ||
"language": "python", | ||
"name": "python3" | ||
}, | ||
"language_info": { | ||
"codemirror_mode": { | ||
"name": "ipython", | ||
"version": 3 | ||
}, | ||
"file_extension": ".py", | ||
"mimetype": "text/x-python", | ||
"name": "python", | ||
"nbconvert_exporter": "python", | ||
"pygments_lexer": "ipython3", | ||
"version": "3.8.8" | ||
} | ||
}, | ||
"nbformat": 4, | ||
"nbformat_minor": 5 | ||
} |
Oops, something went wrong.