Skip to content

Commit

Permalink
Merge pull request #88 from pyiron/update_examples
Browse files Browse the repository at this point in the history
Update examples
  • Loading branch information
liamhuber authored Oct 17, 2022
2 parents c67beeb + 8406633 commit 3b31c1c
Show file tree
Hide file tree
Showing 6 changed files with 773 additions and 107 deletions.
88 changes: 86 additions & 2 deletions README.md
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.
199 changes: 199 additions & 0 deletions example.ipynb
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
}
Loading

0 comments on commit 3b31c1c

Please sign in to comment.