Skip to content

Commit

Permalink
Merge pull request #162 from pyiron/onto_codevelopment
Browse files Browse the repository at this point in the history
Onto codevelopment
  • Loading branch information
liamhuber authored Mar 17, 2023
2 parents 1b133b5 + 469646a commit 7a38f6e
Show file tree
Hide file tree
Showing 27 changed files with 2,128 additions and 77 deletions.
2 changes: 2 additions & 0 deletions .binder/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ dependencies:
- matplotlib
- nglview
- numpy
- owlready2
- pandas
- pyiron_base
- pyiron_atomistics >= 0.2.57
- pyiron_gui >= 0.0.8
- pyiron_ontology == 0.1.1
- ryvencore
- seaborn
- traitlets
Expand Down
2 changes: 2 additions & 0 deletions .ci_support/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ dependencies:
- matplotlib
- nglview
- numpy
- owlready2
- pandas
- pyiron_base
- pyiron_atomistics >= 0.2.57
- pyiron_gui >= 0.0.8
- pyiron_ontology == 0.1.1
- ryvencore
- seaborn
- traitlets
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ If there is a particular use-case you'd like to see, or if one of our nodes is n

![](docs/_static/screenshot.png)

In its current form, ironflow has some UI performance issues, especially when placing new nodes with many ports, or moving nodes around in a big graph.
(You can look at the movie demonstrating ontological typing below to get a sense of the delay for placing larger nodes.)
This is a [known issue](https://github.com/pyiron/ironflow/issues/143) and performance enhancements are currently our top priority -- both in terms of UI performance and underlying computations, e.g. we would like to exploit the latest pyiron developments for [running lammps without writing any files](https://github.com/pyiron/pyiron_lammps) in calculation nodes.

## Usage

The main gui can be imported directly from `ironflow`.
Expand Down Expand Up @@ -58,7 +62,7 @@ Ironflow is built on top of ryvencore 0.3.1.1.
There are a number of minor differences between ryven nodes and ironflow nodes discussed in the next section, but at a
high level there are two significant differences:

### Typing
### Data typing

All node ports are typed, and connection perform type-checking to ensure validity prior to establishing a connection.
By default, a special `Untyped` data type is used, which performs *all* validity checks by value, and thus does not allow pre-wiring of a graph without full data.
Expand All @@ -71,6 +75,29 @@ An output port can be connected to an input port as long as its valid classes ar
This type checking is still under development and may be somewhat brittle.
Our goal is to extend this system to be dynamically informed by an ontology on top of the graph: instead of statically insisting that input be of type `float`, we instead demand that the ontological type of the energy be `surface energy` _dynamically_ because the output value of that port is used, e.g., to calculate a grain boundary interface energy.

### Ontological typing

Nodes can also optionally carry an "ontological type" (otype).
Leaning on the [pyiron_ontology](https://github.com/pyiron/pyiron_ontology) library for representing knowledge in computational workflows, otypes give a rich _graph dependent_ representation of the data and facilitate guided workflow design.
This is fully demonstrated in the `bulk_modulus.ipynb` and `surface_energy.ipynb` notebooks, but a quick demo is also provided in the video below.

We see that there is a "recommended" tab for nodes.
After selecting this menu, clicking on the `CalcMurnaghan.engine` port populates the tab with nodes that have valid output for this port.
We can double-click to place the new node (`Lammps`) and repeat the process, e.g. for the `Lammps.structure` input.
Here we see there are two possibilities -- `BulkStructure` and `SlabStructure` -- and place both.
(Note, as mentioned at the head of the readme, there is some lag in ironflow right now; you can see this in the delay between the double-click and the placement of these larger nodes.)
Not only do we get recommendations for nodes to place in the graph, but we also get specific recommendations of which ports make valid connections!
Below we again select the `Lammps.structure` input port, and see that the output ports on both the structure nodes is highlighted.
Similarly, if we click the `Lammps.engine` output port, we see that all the valid input ports on our graph get highlighted; in this case, `CalcMurnaghan.input`.
Finally, we see the real power of otypes -- by connecting the two `engine` ports, the `Lammps` node now has access to the _ontological requirements_ of the `CalcMurnaghan` node!
In particular, `CalcMurnaghan` produces _bulk moduli_ and thus only works for calculations on _bulk structures_.
After these are connected, when we once again select the `Lammps.structure` input, _only_ the `BulkStructure` node gets highlighted, and _only_ `BulkStructure` appears in the recommended nodes window.

![ironflow_ontology.mov](docs/_static/ironflow_ontology.mov)

Of course, not all ports in ironflow are otyped, and indeed not all should be -- e.g. it doesn't make sense to ontologically-type the output of the `Linspace` node, as it is just providing numbers which may be useful in many contexts.
However, for nodes which specifically produce and require physically-/ontologically-meaningful data, otyping is a powerful tool for understanding workflows and guiding their design.

### Batching

Many ports can be "batched" by selecting them to open the node controller window and pressing the "batched" button.
Expand Down Expand Up @@ -117,12 +144,13 @@ class My_Node(Node):
gui.register_node(My_Node)
```

Ironflow nodes differ from standard ryven (version 0.3.1.1) nodes in four ways:
Ironflow nodes differ from standard ryven (version 0.3.1.1) nodes in five 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.
- Input/output ports and the port values are directly accessible as attributes *if* those ports were labeled, e.g. `node.inputs.ports.foo` or `node.outputs.values.bar`.
- They have a `representation` dictionary, which is used by the IPython gui front-end to give a richer look at nodes. By default, this includes all the outputs and the source code for the node, but you can append to or overwrite these values by specifying an `extra_representations` dictionary on your custom nodes.
- 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.)
- It is strongly advised to specify a `dtype` for each of your nodes from among `node_tools.dtypes`.
- Ports have an additional `otype` field to facilitate ontologically-informed port and node suggestions.

Otherwise, they are just standard ryven nodes, and all the ryven documentation applies.

Expand Down
Binary file added docs/_static/ironflow_ontology.mov
Binary file not shown.
2 changes: 2 additions & 0 deletions docs/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ dependencies:
- matplotlib
- nglview
- numpy
- owlready2
- pandas
- pyiron_base
- pyiron_atomistics >= 0.2.57
- pyiron_gui >= 0.0.8
- pyiron_ontology == 0.1.1
- ryvencore
- seaborn
- traitlets
11 changes: 10 additions & 1 deletion ironflow/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

if TYPE_CHECKING:
from ironflow.model.node import Node
from ironflow.model.port import NodeInput, NodeOutput


class GUI(HasSession, DrawsWidgets):
Expand Down Expand Up @@ -85,7 +86,7 @@ def __init__(
**kwargs,
)

self.workflows = WorkflowsGUI(model=self)
self.workflows = WorkflowsGUI(gui=self)
self.browser = BrowserGUI()

try:
Expand Down Expand Up @@ -158,6 +159,14 @@ def log_to_display(self):
def log_to_stdout(self):
self.log.log_to_stdout()

def build_recommendations(self, port: NodeInput | NodeOutput):
self.recommend_nodes(port)
self.workflows.flow_box.update_nodes(self.nodes_dictionary)

def clear_recommendations(self):
self.clear_recommended_nodes()
self.workflows.flow_box.update_nodes(self.nodes_dictionary)

def draw(self):
return self.widget

Expand Down
1 change: 1 addition & 0 deletions ironflow/gui/workflows/boxes/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def nodes_options(self) -> list[str]:
def update(self, nodes_dictionary: dict) -> None:
self._nodes_dictionary = nodes_dictionary
self.modules_dropdown.options = self.module_options
self.node_selector.options = self.nodes_options


class FlowBox(DrawsWidgets):
Expand Down
5 changes: 5 additions & 0 deletions ironflow/gui/workflows/canvas_widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from ironflow.gui.workflows.canvas_widgets.flow import FlowCanvas
from ironflow.gui.workflows.screen import WorkflowsGUI
from ironflow.model.flow import Flow
from ironflow.model.model import HasSession
from ironflow.gui.workflows.canvas_widgets.layouts import Layout


Expand Down Expand Up @@ -74,6 +75,10 @@ def y(self) -> Number:
def canvas(self) -> Canvas:
return self.parent.canvas

@property
def gui(self) -> HasSession:
return self.parent.gui

@property
def screen(self) -> WorkflowsGUI:
return self.parent.screen
Expand Down
56 changes: 56 additions & 0 deletions ironflow/gui/workflows/canvas_widgets/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
from ipycanvas import Canvas, hold_canvas
from IPython.display import display

from ironflow.model.port import NodeInput, NodeOutput
from ironflow.gui.workflows.canvas_widgets.base import CanvasWidget
from ironflow.gui.workflows.canvas_widgets.layouts import NodeLayout
from ironflow.gui.workflows.canvas_widgets.nodes import NodeWidget
from ironflow.gui.workflows.canvas_widgets.ports import PortWidget

if TYPE_CHECKING:
from ironflow.gui.gui import GUI
from ironflow.gui.workflows.canvas_widgets.base import Number
from ironflow.gui.workflows.screen import WorkflowsGUI
from ironflow.model.flow import Flow
Expand Down Expand Up @@ -88,6 +90,7 @@ def __init__(self, screen: WorkflowsGUI, flow: Flow):
)

self._object_to_gui_dict = {}
self._highlighted_ports: list[PortWidget] = []

@property
def canvas(self):
Expand All @@ -97,6 +100,10 @@ def canvas(self):
def flow_canvas(self) -> FlowCanvas:
return self

@property
def gui(self) -> GUI:
return self.screen.gui

@property
def title(self) -> str:
return self.flow.script.title
Expand Down Expand Up @@ -253,3 +260,52 @@ def zoom_in(self) -> None:

def zoom_out(self) -> None:
self._zoom(min(self._zoom_index + 1, len(self._zoom_factors) - 1))

def highlight_compatible_ports(self, selected: PortWidget):
if selected.port.otype is None:
return

compatible_port_widgets = self._get_port_widgets_ontologically_compatible_with(
selected.port
)

for port_widget in compatible_port_widgets:
port_widget.highlight()
self._highlighted_ports = compatible_port_widgets

def _get_port_widgets_ontologically_compatible_with(self, port):
if isinstance(port, NodeInput):
input_tree = port.otype.get_source_tree(
additional_requirements=port.get_downstream_requirements()
)
return [
subwidget
for subwidget in self._port_widgets
if isinstance(subwidget.port, NodeOutput)
and subwidget.port.all_connections_found_in(input_tree)
]
elif isinstance(port, NodeOutput):
return [
subwidget
for subwidget in self._port_widgets
if subwidget.port.otype is not None # Progressively expensive checks
and port.otype in subwidget.port.otype.get_sources()
and subwidget.port.workflow_tree_contains_connections_of(port)
]
else:
raise TypeError(
f"Expected a {NodeInput} or {NodeOutput} but got {type(port)}"
)

@property
def _port_widgets(self):
return [
subwidget
for node_widget in self.objects_to_draw
for subwidget in node_widget.objects_to_draw
if isinstance(subwidget, PortWidget)
]

def clear_port_highlighting(self):
for port_widget in self._highlighted_ports:
port_widget.dehighlight()
1 change: 1 addition & 0 deletions ironflow/gui/workflows/canvas_widgets/layouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class PortLayout(Layout, ABC):
width: int = 20
height: int = 20
max_title_chars: int = 10
highlight_color = "white"


@dataclass
Expand Down
2 changes: 1 addition & 1 deletion ironflow/gui/workflows/canvas_widgets/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
if TYPE_CHECKING:
from ironflow.gui.workflows.canvas_widgets.flow import FlowCanvas
from ironflow.gui.workflows.canvas_widgets.base import Number
from ironflow.model import NodeInputBP, NodeOutputBP
from ironflow.model.port import NodeInputBP, NodeOutputBP
from ironflow.model.node import Node


Expand Down
20 changes: 19 additions & 1 deletion ironflow/gui/workflows/canvas_widgets/ports.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def __init__(
self.radius = radius
self.port = port
self.title_alignment = title_alignment
self.highlighted = False

def on_click(
self, last_selected_object: Optional[CanvasWidget]
Expand All @@ -66,7 +67,9 @@ def on_click(

@property
def _current_color(self):
if self.port.valid_val:
if self.highlighted:
color = self.layout.highlight_color
elif self.port.valid_val:
if self.selected:
color = self.layout.valid_selected_color
else:
Expand Down Expand Up @@ -105,3 +108,18 @@ def draw_title(self) -> None:

def _is_at_xy(self, x_in: Number, y_in: Number) -> bool:
return (x_in - self.x) ** 2 + (y_in - self.y) ** 2 < self.radius**2

def select(self) -> None:
super().select()
self.gui.build_recommendations(self.port)
self.flow_canvas.highlight_compatible_ports(self)

def deselect(self) -> None:
super().deselect()
self.flow_canvas.clear_port_highlighting()

def highlight(self):
self.highlighted = True

def dehighlight(self):
self.highlighted = False
Loading

0 comments on commit 7a38f6e

Please sign in to comment.