Repository: pyiron/ironflow (language: python)

Issue:
Ontological connections
Currently, Ryven allows node connections to be made willy-nilly. It would be very nice to have strict type checking when connecting the I/O of two nodes, which could be done as a thin layer on top of Ryven. Following a Zoom call with Joerg, it would be ideal to leverage ontological developments inside pyiron to control these checks. Since getting pyiron to be fully ontological is a very big task, in the short term let's develop an ontological layer on top of the very narrow slice of pyiron for which we have ironflow nodes.

Desired behaviour:
- Node connections between ports are impossible if the types do not match
- Selecting an I/O port highlights all the available O/I ports in the graph which are valid connections
- Selecting an I/O port activates some manner of "suggested node" menu, which shows nodes containing appropriate O/I ports

Catching up to the ontological work that's already been going on, and the architecting the ontology check so it's as loosely coupled to Ryven and the pyiron nodes as possible will all take some thinking time, so this feature won't be immediately available.


Discussion / hints:
Idea: let the `Result` node have a dropdown/search window that lets you go through the ontology to select the class of value you are searching for. Then when you click on the input port to this node, the context-sensitive suggestion model shows you the selection of next-previous nodes necessary to accomplish your goal. This is merely a particular instance of Joerg's ontological goal of being able to write workflows in reverse, but I liked the UX of starting with the results node and how it integrates with our ideas about port highlights, so I wanted to make a quick note.

Fix patch:
diff --git a/ironflow/custom_nodes/__init__.py b/ironflow/custom_nodes/__init__.py
index 175988a0..7242610b 100644
--- a/ironflow/custom_nodes/__init__.py
+++ b/ironflow/custom_nodes/__init__.py
@@ -26,5 +26,6 @@
 """
 
 import ironflow.custom_nodes.input_widgets
-from ironflow.model import dtypes, NodeInputBP, NodeOutputBP
+from ironflow.model import dtypes, NodeInputBP
 from ironflow.model.node import Node
+from ironflow.model.port import NodeOutputBP
diff --git a/ironflow/gui/workflows/boxes/node_interface/control.py b/ironflow/gui/workflows/boxes/node_interface/control.py
index 1d77e248..41eca730 100644
--- a/ironflow/gui/workflows/boxes/node_interface/control.py
+++ b/ironflow/gui/workflows/boxes/node_interface/control.py
@@ -77,15 +77,23 @@ def input_field_list(self) -> list[list[widgets.Widget]]:
                     if dtype == "Integer":
                         inp_widget = widgets.IntText(
                             value=inp.val,
-                            disabled=False,
                             description="",
                             continuous_update=False,
+                            disabled=len(inp.connections) > 0,
+                        )
+                    elif dtype == "Float":
+                        inp_widget = widgets.FloatText(
+                            value=inp.val,
+                            description="",
+                            continuous_update=False,
+                            disabled=len(inp.connections) > 0,
                         )
                     elif dtype == "Boolean":
                         inp_widget = widgets.Checkbox(
                             value=inp.val,
                             indent=False,
                             description="",
+                            disabled=len(inp.connections) > 0,
                         )
                     elif dtype == "Choice":
                         inp_widget = widgets.Dropdown(
@@ -93,12 +101,17 @@ def input_field_list(self) -> list[list[widgets.Widget]]:
                             options=inp.dtype.items,
                             description="",
                             ensure_option=True,
+                            disabled=len(inp.connections) > 0,
                         )
-
-                    else:
+                    elif dtype == "String" or dtype == "Char":
                         inp_widget = widgets.Text(
                             value=str(inp.val),
                             continuous_update=False,
+                            disabled=len(inp.connections) > 0,
+                        )
+                    else:
+                        inp_widget = widgets.Text(
+                            value=str(inp.val), continuous_update=False, disabled=True
                         )
                     description = inp.label_str
                 elif inp.label_str != "":
diff --git a/ironflow/gui/workflows/canvas_widgets/base.py b/ironflow/gui/workflows/canvas_widgets/base.py
index 404e5192..101bff67 100644
--- a/ironflow/gui/workflows/canvas_widgets/base.py
+++ b/ironflow/gui/workflows/canvas_widgets/base.py
@@ -18,7 +18,7 @@
 if TYPE_CHECKING:
     from ironflow.gui.workflows.canvas_widgets.flow import FlowCanvas
     from ironflow.gui.workflows.screen import WorkflowsGUI
-    from ironflow.model import Flow
+    from ironflow.model.flow import Flow
 
 
 class CanvasWidget(ABC):
diff --git a/ironflow/gui/workflows/canvas_widgets/buttons.py b/ironflow/gui/workflows/canvas_widgets/buttons.py
index c2d508e3..afedda90 100644
--- a/ironflow/gui/workflows/canvas_widgets/buttons.py
+++ b/ironflow/gui/workflows/canvas_widgets/buttons.py
@@ -10,9 +10,9 @@
 from abc import ABC, abstractmethod
 from typing import TYPE_CHECKING, Optional
 
+from ironflow.model.port import NodeInput, NodeOutput
 from ironflow.gui.workflows.canvas_widgets.base import CanvasWidget, HideableWidget
 from ironflow.gui.workflows.canvas_widgets.layouts import ButtonLayout
-from ironflow.model import NodeInput, NodeOutput
 
 if TYPE_CHECKING:
     from ironflow.gui.workflows.canvas_widgets.base import Number
diff --git a/ironflow/gui/workflows/canvas_widgets/flow.py b/ironflow/gui/workflows/canvas_widgets/flow.py
index 1c4696df..a32da1a2 100644
--- a/ironflow/gui/workflows/canvas_widgets/flow.py
+++ b/ironflow/gui/workflows/canvas_widgets/flow.py
@@ -22,7 +22,7 @@
 if TYPE_CHECKING:
     from ironflow.gui.workflows.canvas_widgets.base import Number
     from ironflow.gui.workflows.screen import WorkflowsGUI
-    from ironflow.model import Flow
+    from ironflow.model.flow import Flow
     from ironflow.model.node import Node
 
 
diff --git a/ironflow/gui/workflows/canvas_widgets/layouts.py b/ironflow/gui/workflows/canvas_widgets/layouts.py
index 1afa0f61..e77a10f5 100644
--- a/ironflow/gui/workflows/canvas_widgets/layouts.py
+++ b/ironflow/gui/workflows/canvas_widgets/layouts.py
@@ -45,14 +45,17 @@ class PortLayout(Layout, ABC):
 
 @dataclass
 class DataPortLayout(PortLayout):
-    background_color: str = "lightgreen"
-    selected_color: str = "darkgreen"
+    valid_color: str = "lightgreen"
+    valid_selected_color: str = "darkgreen"
+    invalid_color: str = "red"
+    invalid_selected_color: str = "darkred"
 
 
 @dataclass
 class ExecPortLayout(PortLayout):
-    background_color: str = "lightblue"
-    selected_color: str = "darkblue"
+    # Exec ports have no data, so are always valid
+    valid_color: str = "lightblue"
+    valid_selected_color: str = "darkblue"
 
 
 @dataclass
diff --git a/ironflow/gui/workflows/canvas_widgets/ports.py b/ironflow/gui/workflows/canvas_widgets/ports.py
index 74ad1c57..82a71761 100644
--- a/ironflow/gui/workflows/canvas_widgets/ports.py
+++ b/ironflow/gui/workflows/canvas_widgets/ports.py
@@ -15,7 +15,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 NodePort
+    from ironflow.model.port import NodeInput, NodeOutput
 
 
 class PortWidget(HideableWidget):
@@ -25,7 +25,7 @@ def __init__(
         y: Number,
         parent: FlowCanvas | CanvasWidget,
         layout: PortLayout,
-        port: NodePort,
+        port: NodeInput | NodeOutput,
         selected: bool = False,
         title: Optional[str] = None,
         hidden_x: Optional[Number] = None,
@@ -64,12 +64,22 @@ def on_click(
             self.select()
             return self
 
+    @property
+    def _current_color(self):
+        if self.port.valid_val:
+            if self.selected:
+                color = self.layout.valid_selected_color
+            else:
+                color = self.layout.valid_color
+        else:
+            if self.selected:
+                color = self.layout.invalid_selected_color
+            else:
+                color = self.layout.invalid_color
+        return color
+
     def draw_shape(self) -> None:
-        self.canvas.fill_style = (
-            self.layout.selected_color
-            if self.selected
-            else self.layout.background_color
-        )
+        self.canvas.fill_style = self._current_color
         self.canvas.fill_circle(self.x, self.y, self.radius)
 
     def draw_title(self) -> None:
diff --git a/ironflow/gui/workflows/screen.py b/ironflow/gui/workflows/screen.py
index 7e687fe9..fe0b9982 100644
--- a/ironflow/gui/workflows/screen.py
+++ b/ironflow/gui/workflows/screen.py
@@ -24,7 +24,7 @@
 if TYPE_CHECKING:
     from ironflow.model.model import HasSession
     from ironflow.gui.workflows.canvas_widgets.nodes import NodeWidget
-    from ironflow.model import Flow
+    from ironflow.model.flow import Flow
     from ironflow.model.node import Node
 
 
diff --git a/ironflow/model/__init__.py b/ironflow/model/__init__.py
index 6e561df4..23f9d461 100644
--- a/ironflow/model/__init__.py
+++ b/ironflow/model/__init__.py
@@ -3,11 +3,13 @@
 # Distributed under the terms of "New BSD License", see the LICENSE file.
 """
 Wrappers and extensions of the underlying ryven model.
+
+The `model.HasSession` class provides the main interface between ironflow and ryven.
+The rest of these modules revolve mostly around the `DType` class -- namely, ensuring
+that each port has a `dtype` attribute (`None` values are handled fine to describe
+un-typed ports) and that these dtypes can be used for meaningful type checking when
+making connections.
 """
 
-from ryvencore import (
-    Flow,
-    NodeInputBP,
-    NodeOutputBP,
-)
-from ryvencore.NodePort import NodePort, NodeInput, NodeOutput
+from ryvencore import NodeInputBP
+from ryvencore.NodePort import NodePort
diff --git a/ironflow/model/dtypes.py b/ironflow/model/dtypes.py
index 0a1b200a..5ffb1ade 100644
--- a/ironflow/model/dtypes.py
+++ b/ironflow/model/dtypes.py
@@ -7,9 +7,201 @@
 Implementation changes in ryvencore v0.4, so this file may be short-lived.
 """
 
-from typing import Optional
+from __future__ import annotations
 
-from ryvencore.dtypes import DType, Data, Integer, Float, String, Boolean, Char
+from typing import Any, Optional
+
+from ryvencore.dtypes import DType as DTypeCore
+
+
+class DType(DTypeCore):
+    def __init__(
+        self,
+        default,
+        bounds: tuple = None,
+        doc: str = "",
+        _load_state=None,
+        valid_classes=None,
+        allow_none=False,
+    ):
+        super().__init__(
+            default=default,
+            bounds=bounds,
+            doc=doc,
+            _load_state=_load_state,
+        )
+        if _load_state is None:
+            if isinstance(valid_classes, list):
+                self.valid_classes = list(valid_classes)
+            elif valid_classes is not None:
+                self.valid_classes = [valid_classes]
+            else:
+                self.valid_classes = []
+            self.allow_none = allow_none
+        self.add_data("valid_classes")
+        self.add_data("allow_none")
+
+    @staticmethod
+    def from_str(s):
+        # Load local dtypes, not ryven dtypes
+        for DTypeClass in [Boolean, Char, Choice, Data, Float, Integer, List, String]:
+            if s == "DType." + DTypeClass.__name__:
+                return DTypeClass
+
+        return None
+
+    def _dtype_matches(self, val: DType):
+        if isinstance(val, self.__class__):  # DType must be subclass
+            other_is_more_specific = all(
+                [
+                    any([issubclass(o, ref) for ref in self.valid_classes])
+                    for o in val.valid_classes
+                ]
+            )
+            might_get_surprising_none = val.allow_none and not self.allow_none
+            return other_is_more_specific and not might_get_surprising_none
+        else:
+            return False
+
+    def _instance_matches(self, val: Any):
+        return any([isinstance(val, c) for c in self.valid_classes])
+
+    def matches(self, val: DType | Any | None):
+        if isinstance(val, DType):
+            return self._dtype_matches(val)
+        elif val is not None:
+            return self._instance_matches(val)
+        else:
+            return self.allow_none
+
+
+class Data(DType):
+    """Any kind of data represented by some evaluated text input"""
+
+    def __init__(
+        self,
+        default=None,
+        size: str = "m",
+        doc: str = "",
+        _load_state=None,
+        valid_classes=None,
+        allow_none=False,
+    ):
+        """
+        size: 's' / 'm' / 'l'
+        """
+        self.size = size
+        super().__init__(
+            default=default,
+            doc=doc,
+            _load_state=_load_state,
+            valid_classes=valid_classes,
+            allow_none=allow_none,
+        )
+        self.add_data("size")
+
+
+class Integer(DType):
+    def __init__(
+        self,
+        default: int = 0,
+        bounds: tuple = None,
+        doc: str = "",
+        _load_state=None,
+        valid_classes=None,
+        allow_none=False,
+    ):
+        super().__init__(
+            default=default,
+            bounds=bounds,
+            doc=doc,
+            _load_state=_load_state,
+            valid_classes=int if valid_classes is None else valid_classes,
+            allow_none=allow_none,
+        )
+
+
+class Float(DType):
+    def __init__(
+        self,
+        default: float = 0.0,
+        bounds: tuple = None,
+        decimals: int = 10,
+        doc: str = "",
+        _load_state=None,
+        valid_classes=None,
+        allow_none=False,
+    ):
+        self.decimals = decimals
+        super().__init__(
+            default=default,
+            bounds=bounds,
+            doc=doc,
+            _load_state=_load_state,
+            valid_classes=float if valid_classes is None else valid_classes,
+            allow_none=allow_none,
+        )
+        self.add_data("decimals")
+
+
+class Boolean(DType):
+    def __init__(
+        self,
+        default: bool = False,
+        doc: str = "",
+        _load_state=None,
+        valid_classes=None,
+        allow_none=False,
+    ):
+        super().__init__(
+            default=default,
+            doc=doc,
+            _load_state=_load_state,
+            valid_classes=bool if valid_classes is None else valid_classes,
+            allow_none=allow_none,
+        )
+
+
+class Char(DType):
+    def __init__(
+        self,
+        default: chr = "",
+        doc: str = "",
+        _load_state=None,
+        valid_classes=None,
+        allow_none=False,
+    ):
+        super().__init__(
+            default=default,
+            doc=doc,
+            _load_state=_load_state,
+            valid_classes=chr if valid_classes is None else valid_classes,
+            allow_none=allow_none,
+        )
+
+
+class String(DType):
+    def __init__(
+        self,
+        default: str = "",
+        size: str = "m",
+        doc: str = "",
+        _load_state=None,
+        valid_classes=None,
+        allow_none=False,
+    ):
+        """
+        size: 's' / 'm' / 'l'
+        """
+        self.size = size
+        super().__init__(
+            default=default,
+            doc=doc,
+            _load_state=_load_state,
+            valid_classes=str if valid_classes is None else valid_classes,
+            allow_none=allow_none,
+        )
+        self.add_data("size")
 
 
 class Choice(DType):
@@ -19,13 +211,37 @@ def __init__(
         items: Optional[list] = None,
         doc: str = "",
         _load_state=None,
+        valid_classes=None,
+        allow_none=False,
     ):
         self.items = items if items is not None else []
-        super().__init__(default=default, doc=doc, _load_state=_load_state)
+        super().__init__(
+            default=default,
+            doc=doc,
+            _load_state=_load_state,
+            valid_classes=valid_classes,
+            allow_none=allow_none,
+        )
         self.add_data("items")
 
+    def _instance_matches(self, val: Any):
+        return val in self.items
+
 
 class List(DType):
-    def __init__(self, default: Optional[list] = None, doc: str = "", _load_state=None):
+    def __init__(
+        self,
+        default: Optional[list] = None,
+        doc: str = "",
+        _load_state=None,
+        valid_classes=None,
+        allow_none=False,
+    ):
         default = default if default is not None else []
-        super().__init__(default=default, doc=doc, _load_state=_load_state)
+        super().__init__(
+            default=default,
+            doc=doc,
+            _load_state=_load_state,
+            valid_classes=list if valid_classes is None else valid_classes,
+            allow_none=allow_none,
+        )
diff --git a/ironflow/model/flow.py b/ironflow/model/flow.py
new file mode 100644
index 00000000..a673d303
--- /dev/null
+++ b/ironflow/model/flow.py
@@ -0,0 +1,44 @@
+from __future__ import annotations
+
+from ryvencore import Flow as FlowCore, InfoMsgs
+from ryvencore.NodePort import NodePort
+
+
+class Flow(FlowCore):
+    """
+    Wraps `ryvencore.Flow.Flow` to override the connection validity check, so we can
+    add type checking.
+    """
+
+    def check_connection_validity(self, p1: NodePort, p2: NodePort) -> bool:
+        """Checks whether a considered connect action is legal"""
+
+        # ryvencore.Flow.Flow content
+        valid = True
+
+        if p1.node == p2.node:
+            valid = False
+
+        if p1.io_pos == p2.io_pos or p1.type_ != p2.type_:
+            valid = False
+
+        # ironflow content
+        inp, out = (p1, p2) if p1.io_pos == 1 else (p2, p1)
+        if inp.dtype is not None:
+            if out.dtype is not None:
+                valid = inp.dtype.matches(out.dtype)
+                InfoMsgs.write(
+                    f"dtype-dtype check for {inp.node.title}.{inp.label_str} and "
+                    f"{out.node.title}.{out.label_str} returned {valid}"
+                )
+            else:
+                valid = inp.dtype.matches(out.val)
+                InfoMsgs.write(
+                    f"dtype-value check for {inp.node.title}.{inp.label_str} and "
+                    f"{out.node.title}.{out.label_str} returned {valid}"
+                )
+
+        # ryvencore.Flow.Flow content
+        self.connection_request_valid.emit(valid)
+
+        return valid
diff --git a/ironflow/model/model.py b/ironflow/model/model.py
index ee12419c..5376f9d6 100644
--- a/ironflow/model/model.py
+++ b/ironflow/model/model.py
@@ -16,9 +16,10 @@
 from types import ModuleType
 from typing import Optional, Type
 
-from ryvencore import Session, Script, Flow
-
 from ironflow.model.node import Node
+from ironflow.model.session import Session
+from ironflow.model.flow import Flow
+from ironflow.model.script import Script
 
 
 class HasSession(ABC):
diff --git a/ironflow/model/node.py b/ironflow/model/node.py
index 129a0689..cbe0e8ac 100644
--- a/ironflow/model/node.py
+++ b/ironflow/model/node.py
@@ -4,19 +4,18 @@
 from __future__ import annotations
 
 import inspect
-from copy import deepcopy
-from typing import TYPE_CHECKING
+from typing import Optional
 
 from ryvencore import Node as NodeCore
 from ryvencore.Base import Event
 from ryvencore.NodePort import NodePort
+from ryvencore.utils import deserialize
 
 from ironflow.gui.workflows.canvas_widgets.nodes import NodeWidget
+from ironflow.model.dtypes import DType
+from ironflow.model.port import NodeInput, NodeOutput
 from ironflow.utils import display_string
 
-if TYPE_CHECKING:
-    from ironflow.model.dtypes import DType
-
 
 class PortList(list):
     """
@@ -63,20 +62,29 @@ class PortFinder:
     def __init__(self, port_list: PortList):
         self._port_list = port_list
 
+    @property
+    def _filtered_port_list(self):
+        return [item for item in self._port_list if isinstance(item, NodePort)]
+
     def __getattr__(self, key):
-        for node_port in [
-            item for item in self._port_list if isinstance(item, NodePort)
-        ]:
+        for node_port in self._filtered_port_list:
             if node_port.label_str == key:
                 return node_port
         raise AttributeError(f"No port found with the label {key}")
 
+    def __iter__(self):
+        return self._filtered_port_list.__iter__()
+
 
 class ValueFinder(PortFinder):
     def __getattr__(self, key):
         node_port = super().__getattr__(key)
         return node_port.val
 
+    def __iter__(self):
+        val_list = [p.val for p in self._filtered_port_list]
+        return val_list.__iter__()
+
 
 class Node(NodeCore):
     """
@@ -127,21 +135,98 @@ def __init__(self, params):
         self.representation_updated = False
         self.after_update.connect(self._representation_update)
 
+    def _add_io(self, io_group, new_io, insert: int = None):
+        if insert is not None:
+            io_group.insert(insert, new_io)
+        else:
+            io_group.append(new_io)
+
     def create_input(
-        self, label: str = "", type_: str = "data", add_data=None, insert: int = None
+        self,
+        type_: str = "data",
+        label: str = "",
+        add_data: Optional[dict] = None,
+        dtype: Optional[DType] = None,
+        insert: Optional[int] = None,
     ):
-        add_data = add_data if add_data is not None else {}
-        super().create_input(label=label, type_=type_, add_data=add_data, insert=insert)
+        """Creates and add a new input port"""
+        inp = NodeInput(
+            node=self, type_=type_, label_str=label, add_data=add_data, dtype=dtype
+        )
+        self._add_io(self.inputs, inp, insert=insert)
 
     def create_input_dt(
-        self, dtype: DType, label: str = "", add_data=None, insert: int = None
+        self, dtype: DType, label: str = "", add_data={}, insert: int = None
     ):
-        """Be more careful with mutables"""
-        add_data = add_data if add_data is not None else {}
-        super().create_input_dt(
-            dtype=deepcopy(dtype), label=label, add_data=add_data, insert=insert
+        raise RuntimeError(
+            "Ironflow uses custom NodePort classes and this method is not supported. "
+            "Please use create_input instead."
         )
 
+    def create_output(
+        self,
+        type_: str = "data",
+        label: str = "",
+        dtype: Optional[DType] = None,
+        insert: Optional[int] = None,
+    ):
+        """Create and add a new output port"""
+        out = NodeOutput(node=self, type_=type_, label_str=label, dtype=dtype)
+        self._add_io(self.outputs, out, insert=insert)
+
+    def setup_ports(self, inputs_data=None, outputs_data=None):
+        # A streamlined version of the ryvencore method which exploits our NodeInput
+        # and NodeOutput classes instead.
+        if not inputs_data and not outputs_data:
+
+            for i in range(len(self.init_inputs)):
+                inp = self.init_inputs[i]
+                self.create_input(
+                    type_=inp.type_,
+                    label=inp.label,
+                    add_data=inp.add_data,
+                    dtype=inp.dtype,
+                )
+
+            for o in range(len(self.init_outputs)):
+                out = self.init_outputs[o]
+                self.create_output(type_=out.type_, label=out.label, dtype=out.dtype)
+
+        else:
+            # load from data
+            # initial ports specifications are irrelevant then
+
+            for inp in inputs_data:
+                if "dtype" in inp:
+                    dtype = DType.from_str(inp["dtype"])(
+                        _load_state=deserialize(inp["dtype state"])
+                    )
+                    self.create_input(label=inp["label"], add_data=inp, dtype=dtype)
+                else:
+                    self.create_input(
+                        type_=inp["type"], label=inp["label"], add_data=inp
+                    )
+
+                if "val" in inp:
+                    # this means the input is 'data' and did not have any connections,
+                    # so we saved its value which was probably represented by some widget
+                    # in the front end which has probably overridden the Node.input() method
+                    self.inputs[-1].val = deserialize(inp["val"])
+
+            # ironflow modification
+            for out in outputs_data:
+                if "dtype" in out:
+                    dtype = DType.from_str(out["dtype"])(
+                        _load_state=deserialize(out["dtype state"])
+                    )
+                    self.create_output(label=out["label"], dtype=dtype)
+                else:
+                    self.create_output(type_=out["type"], label=out["label"])
+
+    @property
+    def all_input_is_valid(self):
+        return all([p.valid_val for p in self.inputs.ports])
+
     def place_event(self):
         # place_event() is executed *before* the connections are built
         super().place_event()
@@ -166,6 +251,14 @@ def output(self, i):
     def _representation_update(self, inp):
         self.representation_updated = True
 
+    @property
+    def _source_code(self):
+        try:
+            return inspect.getsource(self.__class__)
+        except TypeError:
+            # Classes defined in the notebook can't access their source this way
+            return ""
+
     @property
     def _standard_representations(self):
         standard_reps = {
@@ -173,7 +266,7 @@ def _standard_representations(self):
             for i, o in enumerate(self.outputs)
             if o.type_ == "data"
         }
-        standard_reps["source code"] = display_string(inspect.getsource(self.__class__))
+        standard_reps["source code"] = display_string(self._source_code)
         return standard_reps
 
     @property
diff --git a/ironflow/model/port.py b/ironflow/model/port.py
new file mode 100644
index 00000000..cb6653a8
--- /dev/null
+++ b/ironflow/model/port.py
@@ -0,0 +1,77 @@
+# coding: utf-8
+# Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department
+# Distributed under the terms of "New BSD License", see the LICENSE file.
+"""
+A new output port class that has a dtype.
+"""
+
+from __future__ import annotations
+
+from copy import deepcopy
+from typing import Optional, TYPE_CHECKING
+
+from ryvencore.NodePort import NodeInput as NodeInputCore, NodeOutput as NodeOutputCore
+from ryvencore.NodePortBP import NodeOutputBP as NodeOutputBPCore
+from ryvencore.utils import serialize
+
+from ironflow.model.dtypes import DType
+
+if TYPE_CHECKING:
+    from ironflow.model.node import Node
+
+
+class HasDType:
+    """A mixin to add the valid value check property"""
+
+    @property
+    def valid_val(self):
+        if self.dtype is not None:
+            if self.val is not None:
+                return self.dtype._instance_matches(self.val)
+            else:
+                return self.dtype.allow_none
+        else:
+            return True
+
+
+class NodeInput(NodeInputCore, HasDType):
+    def __init__(
+        self,
+        node: Node,
+        type_: str = "data",
+        label_str: str = "",
+        add_data: Optional[dict] = None,
+        dtype: Optional[DType] = None,
+    ):
+        super().__init__(
+            node=node,
+            type_=type_ if dtype is None else "data",
+            label_str=label_str,
+            add_data=add_data if add_data is not None else {},
+            dtype=deepcopy(dtype),  # Because some dtypes have mutable fields
+        )
+
+
+class NodeOutput(NodeOutputCore, HasDType):
+    def __init__(self, node, type_="data", label_str="", dtype: DType = None):
+        super().__init__(
+            node=node, type_=type_ if dtype is None else "data", label_str=label_str
+        )
+        self.dtype = deepcopy(dtype)  # Some dtypes have mutable fields
+
+    def data(self) -> dict:
+        data = super().data()
+
+        if self.dtype is not None:
+            data["dtype"] = str(self.dtype)
+            data["dtype state"] = serialize(self.dtype.get_state())
+
+        return data
+
+
+class NodeOutputBP(NodeOutputBPCore):
+    def __init__(
+        self, label: str = "", type_: str = "data", dtype: Optional[DType] = None
+    ):
+        super().__init__(label=label, type_=type_)
+        self.dtype = dtype
diff --git a/ironflow/model/script.py b/ironflow/model/script.py
new file mode 100644
index 00000000..8ec870cc
--- /dev/null
+++ b/ironflow/model/script.py
@@ -0,0 +1,32 @@
+from __future__ import annotations
+
+from typing import Optional, TYPE_CHECKING
+
+from ryvencore import Script as ScriptCore
+
+from ironflow.model.flow import Flow
+
+if TYPE_CHECKING:
+    from ironflow.model.session import Session
+
+
+class Script(ScriptCore):
+    """
+    Wraps `ryvencore.Script.Script` to build our `Flow` class instead.
+    """
+
+    def __init__(
+        self,
+        session: Session,
+        title: Optional[str] = None,
+        load_data: Optional[dict] = None,
+        create_default_logs: bool = True,
+    ):
+        super().__init__(
+            session=session,
+            title=title,
+            load_data=load_data,
+            create_default_logs=create_default_logs,
+        )
+        del self.flow  # A little inefficient, but I don't want to reproduce init here
+        self.flow = Flow(self.session, self)
diff --git a/ironflow/model/session.py b/ironflow/model/session.py
new file mode 100644
index 00000000..d9e32dca
--- /dev/null
+++ b/ironflow/model/session.py
@@ -0,0 +1,48 @@
+# coding: utf-8
+# Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department
+# Distributed under the terms of "New BSD License", see the LICENSE file.
+"""
+A wrapper for `ryvencore.Session` that makes sure our custom flow class gets
+instantiated
+"""
+
+from __future__ import annotations
+
+from typing import Optional
+
+from ryvencore.Session import Session as SessionCore
+
+from ironflow.model.script import Script
+
+
+class Session(SessionCore):
+    """
+    Wraps `ryvencore.Session.Session` to create our `Script` class instead
+    """
+
+    def create_script(
+        self,
+        title: Optional[str] = None,
+        create_default_logs: bool = True,
+        data: Optional[dict] = None,
+    ) -> Script:
+        """
+        Creates and returns a new script.
+        If data is provided the title parameter will be ignored.
+        """
+        # Exactly the content of ScriptCore.create_script,
+        # but the `Script` class here is different!
+
+        script = Script(
+            session=self,
+            title=title,
+            create_default_logs=create_default_logs,
+            load_data=data,
+        )
+
+        self.scripts.append(script)
+        script.load_flow()
+
+        self.new_script_created.emit(script)
+
+        return script
diff --git a/ironflow/nodes/built_in.py b/ironflow/nodes/built_in.py
index f75cf176..7b060867 100644
--- a/ironflow/nodes/built_in.py
+++ b/ironflow/nodes/built_in.py
@@ -1,5 +1,6 @@
-from ironflow.model import dtypes, NodeInputBP, NodeOutputBP
+from ironflow.model import dtypes, NodeInputBP
 from ironflow.model.node import Node, PlaceholderWidgetsContainer
+from ironflow.model.port import NodeOutputBP
 
 widgets = PlaceholderWidgetsContainer()
 
@@ -88,7 +89,7 @@ class Val_Node(NodeBase):
         NodeInputBP(dtype=dtypes.Data(size="s")),
     ]
     init_outputs = [
-        NodeInputBP(type_="data"),
+        NodeOutputBP(type_="data"),
     ]
     style = "small"
     color = "#c69a15"
diff --git a/ironflow/nodes/pyiron/atomistics_nodes.py b/ironflow/nodes/pyiron/atomistics_nodes.py
index d1a79669..b9a3f3a4 100644
--- a/ironflow/nodes/pyiron/atomistics_nodes.py
+++ b/ironflow/nodes/pyiron/atomistics_nodes.py
@@ -2,7 +2,8 @@
 # Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department
 # Distributed under the terms of "New BSD License", see the LICENSE file.
 """
-Ryven nodes specifc to pyiron (or with ironflow improvements like an ipywidgets representation).
+Ryven nodes specific to pyiron (or with ironflow improvements like an ipywidgets
+representation).
 """
 
 from __future__ import annotations
@@ -13,15 +14,19 @@
 
 import matplotlib.pylab as plt
 import numpy as np
-
-from pyiron_atomistics import Project
+from matplotlib.figure import Figure
+from nglview import NGLWidget
+from pyiron_atomistics import Project, Atoms
 from pyiron_atomistics.atomistics.structure.factory import StructureFactory
 from pyiron_atomistics.atomistics.job.atomistic import AtomisticGenericJob
 from pyiron_atomistics.lammps import list_potentials
+from pyiron_atomistics.lammps.lammps import Lammps
+from ryvencore.InfoMsgs import InfoMsgs
 
 from ironflow.gui.workflows.canvas_widgets.nodes import ButtonNodeWidget
-from ironflow.model import dtypes, NodeInputBP, NodeOutputBP
+from ironflow.model import dtypes, NodeInputBP
 from ironflow.model.node import Node
+from ironflow.model.port import NodeOutputBP
 from ironflow.nodes.std.special_nodes import DualNodeBase
 
 if TYPE_CHECKING:
@@ -75,10 +80,10 @@ class Project_Node(Node):
 
     title = "Project"
     init_inputs = [
-        NodeInputBP(dtype=dtypes.Char(default="."), label="name"),
+        NodeInputBP(dtype=dtypes.String(default="."), label="name"),
     ]
     init_outputs = [
-        NodeOutputBP(label="project"),
+        NodeOutputBP(label="project", dtype=dtypes.Data(valid_classes=Project)),
     ]
     color = "#aabb44"
 
@@ -109,7 +114,7 @@ class OutputsOnlyAtoms(Node, ABC):
     """
 
     init_outputs = [
-        NodeOutputBP(label="structure"),
+        NodeOutputBP(label="structure", dtype=dtypes.Data(valid_classes=Atoms)),
     ]
     color = "#aabb44"
 
@@ -122,7 +127,6 @@ def update_event(self, inp=-1):
     def extra_representations(self) -> dict:
         return {
             "plot3d": self.outputs.values.structure.plot3d(),
-            "print": self.outputs.values.structure,
         }
 
 
@@ -150,7 +154,7 @@ class BulkStructure_Node(OutputsOnlyAtoms):
 
     title = "BulkStructure"
     init_inputs = [
-        NodeInputBP(dtype=dtypes.Char(default="Fe"), label="element"),
+        NodeInputBP(dtype=dtypes.String(default="Fe"), label="element"),
         NodeInputBP(
             dtype=dtypes.Choice(
                 default=None,
@@ -167,31 +171,38 @@ class BulkStructure_Node(OutputsOnlyAtoms):
                     "fluorite",
                     "wurtzite",
                 ],
+                allow_none=True,
             ),
             label="crystal_structure",
         ),
-        NodeInputBP(dtype=dtypes.Float(default=None), label="a"),
-        NodeInputBP(dtype=dtypes.Float(default=None), label="c"),
-        NodeInputBP(dtype=dtypes.Float(default=None), label="c_over_a"),
-        NodeInputBP(dtype=dtypes.Float(default=None), label="u"),
+        NodeInputBP(dtype=dtypes.Float(default=None, allow_none=True), label="a"),
+        NodeInputBP(dtype=dtypes.Float(default=None, allow_none=True), label="c"),
+        NodeInputBP(
+            dtype=dtypes.Float(default=None, allow_none=True), label="c_over_a"
+        ),
+        NodeInputBP(dtype=dtypes.Float(default=None, allow_none=True), label="u"),
         NodeInputBP(dtype=dtypes.Boolean(default=False), label="orthorhombic"),
         NodeInputBP(dtype=dtypes.Boolean(default=False), label="cubic"),
     ]
 
     def update_event(self, inp=-1):
-        self.set_output_val(
-            0,
-            STRUCTURE_FACTORY.bulk(
-                self.inputs.values.element,
-                crystalstructure=self.inputs.values.crystal_structure,
-                a=self.inputs.values.a,
-                c=self.inputs.values.c,
-                covera=self.inputs.values.c_over_a,
-                u=self.inputs.values.u,
-                orthorhombic=self.inputs.values.orthorhombic,
-                cubic=self.inputs.values.cubic,
-            ),
-        )
+        try:
+            self.set_output_val(
+                0,
+                STRUCTURE_FACTORY.bulk(
+                    self.inputs.values.element,
+                    crystalstructure=self.inputs.values.crystal_structure,
+                    a=self.inputs.values.a,
+                    c=self.inputs.values.c,
+                    covera=self.inputs.values.c_over_a,
+                    u=self.inputs.values.u,
+                    orthorhombic=self.inputs.values.orthorhombic,
+                    cubic=self.inputs.values.cubic,
+                ),
+            )
+        except RuntimeError as e:
+            self.set_output_val(0, None)
+            raise e
 
     def place_event(self):
         super().place_event()
@@ -214,7 +225,9 @@ class Repeat_Node(OutputsOnlyAtoms):
 
     title = "Repeat"
     init_inputs = [
-        NodeInputBP(dtype=dtypes.Data(size="m"), label="structure"),
+        NodeInputBP(
+            dtype=dtypes.Data(size="m", valid_classes=Atoms), label="structure"
+        ),
         NodeInputBP(dtype=dtypes.Integer(default=1, bounds=(1, 100)), label="all"),
     ]
 
@@ -238,7 +251,9 @@ class ApplyStrain_Node(OutputsOnlyAtoms):
 
     title = "ApplyStrain"
     init_inputs = [
-        NodeInputBP(dtype=dtypes.Data(size="m"), label="structure"),
+        NodeInputBP(
+            dtype=dtypes.Data(size="m", valid_classes=Atoms), label="structure"
+        ),
         NodeInputBP(dtype=dtypes.Float(default=0, bounds=(-100, 100)), label="strain"),
     ]
 
@@ -261,24 +276,32 @@ class Lammps_Node(Node):
     init_inputs = [
         NodeInputBP(type_="exec", label="run"),
         NodeInputBP(type_="exec", label="remove"),
-        NodeInputBP(dtype=dtypes.Data(size="m"), label="project"),
-        NodeInputBP(dtype=dtypes.Char(default="job"), label="name"),
-        NodeInputBP(dtype=dtypes.Data(size="m"), label="structure"),
+        NodeInputBP(
+            dtype=dtypes.Data(size="m", valid_classes=Project), label="project"
+        ),
+        NodeInputBP(dtype=dtypes.String(default="job"), label="name"),
+        NodeInputBP(
+            dtype=dtypes.Data(size="m", valid_classes=Atoms), label="structure"
+        ),
         NodeInputBP(
             dtype=dtypes.Choice(
-                default="Set structure first", items=["Set structure first"]
+                default="Set structure first",
+                items=["Set structure first"],
+                valid_classes=str,
             ),
             label="potential",
         ),
     ]
     init_outputs = [
-        NodeOutputBP(type_="exec"),
-        NodeOutputBP(label="job"),
+        NodeOutputBP(type_="exec", label="ran"),
+        NodeOutputBP(label="job", dtype=dtypes.Data(valid_classes=Lammps)),
     ]
     color = "#5d95de"
 
     def _run(self):
-        job = self.inputs.values.project.create.job.Lammps(self.inputs.values.name)
+        job = self.inputs.values.project.create.job.Lammps(
+            self.inputs.values.name, delete_existing_job=True
+        )
         job.structure = self.inputs.values.structure
         job.potential = self.inputs.values.potential
         self._job = job
@@ -309,11 +332,11 @@ def _update_potential_choices(self):
             self.inputs.ports.potential.dtype.items = available_potentials
 
     def update_event(self, inp=-1):
-        if inp == 0:
+        if inp == 0 and self.all_input_is_valid:
             self._run()
         elif inp == 1:
             self._remove()
-        elif inp == 4:
+        elif inp == 4 and self.inputs.ports.structure.valid_val:
             self._update_potential_choices()
 
     @property
@@ -337,10 +360,14 @@ class GenericOutput_Node(Node):
     version = "v0.1"
     title = "GenericOutput"
     init_inputs = [
-        NodeInputBP(dtype=dtypes.Data(size="m"), label="job"),
+        NodeInputBP(
+            dtype=dtypes.Data(size="m", valid_classes=AtomisticGenericJob), label="job"
+        ),
         NodeInputBP(
             dtype=dtypes.Choice(
-                default="Input an atomistic job", items=["Input an atomistic job"]
+                default="Input an atomistic job",
+                items=["Input an atomistic job"],
+                valid_classes=str,
             ),
             label="field",
         ),
@@ -428,11 +455,11 @@ class JobName_Node(Node):
 
     title = "JobName"
     init_inputs = [
-        NodeInputBP(dtype=dtypes.Char(default="job_"), label="base"),
-        NodeInputBP(dtype=dtypes.Float(default=0), label="float"),
+        NodeInputBP(dtype=dtypes.String(default="job_"), label="base"),
+        NodeInputBP(dtype=dtypes.Float(default=0.0), label="float"),
     ]
     init_outputs = [
-        NodeOutputBP(label="job_name"),
+        NodeOutputBP(label="job_name", dtype=dtypes.String()),
     ]
     color = "#aabb44"
 
@@ -495,11 +522,17 @@ class Plot3d_Node(Node):
     title = "Plot3d"
     version = "v0.1"
     init_inputs = [
-        NodeInputBP(dtype=dtypes.Data(size="m"), label="structure"),
+        NodeInputBP(
+            dtype=dtypes.Data(size="m", valid_classes=Atoms), label="structure"
+        ),
     ]
     init_outputs = [
-        NodeOutputBP(type_="data", label="plot3d"),
-        NodeOutputBP(type_="data", label="structure"),
+        NodeOutputBP(
+            type_="data", label="plot3d", dtype=dtypes.Data(valid_classes=NGLWidget)
+        ),
+        NodeOutputBP(
+            type_="data", label="structure", dtype=dtypes.Data(valid_classes=Atoms)
+        ),
     ]
     color = "#5d95de"
 
@@ -523,11 +556,11 @@ class Matplot_Node(Node):
     title = "MatPlot"
     version = "v0.1"
     init_inputs = [
-        NodeInputBP(dtype=dtypes.Data(size="m"), label="x"),
-        NodeInputBP(dtype=dtypes.Data(size="m"), label="y"),
+        NodeInputBP(dtype=dtypes.Data(valid_classes=[list, np.ndarray]), label="x"),
+        NodeInputBP(dtype=dtypes.Data(valid_classes=[list, np.ndarray]), label="y"),
     ]
     init_outputs = [
-        NodeOutputBP(type_="data", label="fig"),
+        NodeOutputBP(type_="data", label="fig", dtype=dtypes.Data(valid_classes=Figure))
     ]
     color = "#5d95de"
 
@@ -555,7 +588,10 @@ class Sin_Node(Node):
     title = "Sin"
     version = "v0.1"
     init_inputs = [
-        NodeInputBP(dtype=dtypes.Data(size="m"), label="x"),
+        NodeInputBP(
+            dtype=dtypes.Data(size="m", valid_classes=[int, float, list, np.ndarray]),
+            label="x",
+        ),
     ]
     init_outputs = [
         NodeOutputBP(label="sin"),
diff --git a/ironflow/nodes/std/basic_operators.py b/ironflow/nodes/std/basic_operators.py
index 2f4a45e1..e61d8b7f 100644
--- a/ironflow/nodes/std/basic_operators.py
+++ b/ironflow/nodes/std/basic_operators.py
@@ -1,5 +1,6 @@
-from ironflow.model import dtypes, NodeInputBP, NodeOutputBP
+from ironflow.model import dtypes, NodeInputBP
 from ironflow.model.node import Node
+from ironflow.model.port import NodeOutputBP
 
 
 class OperatorNodeBase(Node):
diff --git a/ironflow/nodes/std/control_structures.py b/ironflow/nodes/std/control_structures.py
index 54b645d5..9adf3043 100644
--- a/ironflow/nodes/std/control_structures.py
+++ b/ironflow/nodes/std/control_structures.py
@@ -1,5 +1,6 @@
-from ironflow.model import dtypes, NodeInputBP, NodeOutputBP
+from ironflow.model import dtypes, NodeInputBP
 from ironflow.model.node import Node
+from ironflow.model.port import NodeOutputBP
 
 
 class CSNodeBase(Node):
diff --git a/ironflow/nodes/std/special_nodes.py b/ironflow/nodes/std/special_nodes.py
index 4bf5d1d0..93f7ea12 100644
--- a/ironflow/nodes/std/special_nodes.py
+++ b/ironflow/nodes/std/special_nodes.py
@@ -1,8 +1,9 @@
 import code
 from contextlib import redirect_stdout, redirect_stderr
 
-from ironflow.model import dtypes, NodeInputBP, NodeOutputBP
+from ironflow.model import dtypes, NodeInputBP
 from ironflow.model.node import Node, PlaceholderWidgetsContainer
+from ironflow.model.port import NodeOutputBP
 from ironflow.gui.workflows.boxes.node_interface.input_widgets import SliderControl
 
 widgets = PlaceholderWidgetsContainer()
diff --git a/notebooks/example.ipynb b/notebooks/example.ipynb
index 049e7d14..0a511247 100644
--- a/notebooks/example.ipynb
+++ b/notebooks/example.ipynb
@@ -13,7 +13,20 @@
    "execution_count": 1,
    "id": "a0a60e2d",
    "metadata": {},
-   "outputs": [],
+   "outputs": [
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "53fdafe5503f4f0091aed589edb23904",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": []
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
    "source": [
     "%matplotlib inline\n",
     "from ironflow import GUI"
@@ -24,15 +37,7 @@
    "execution_count": 2,
    "id": "e8da0476-4a99-4da3-8fe3-e12c646893e8",
    "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "Loaded session data for example\n"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
     "gui = GUI('example')"
    ]
@@ -46,12 +51,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "d91b2f64537f4c3ba7c6711f49b2a20a",
+       "model_id": "65b029668b834a8497617ae2e346ba08",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "VBox(children=(HBox(children=(Dropdown(layout=Layout(width='80px'), options=('data', 'exec'), value='data'), B…"
+       "Tab(children=(VBox(children=(HBox(children=(Dropdown(layout=Layout(width='80px'), options=('data', 'exec'), va…"
       ]
      },
      "metadata": {},
@@ -108,15 +113,7 @@
    "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"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
     "gui2 = GUI(\"example_with_custom_node\", extra_nodes_packages=[[My_Node]])"
    ]
@@ -130,12 +127,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "4fa0213eeaa54243b6493c8f64e7dfdf",
+       "model_id": "291a15ac7e3c4ad2b3bb9f96e2803d8d",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "VBox(children=(HBox(children=(Dropdown(layout=Layout(width='80px'), options=('data', 'exec'), value='data'), B…"
+       "Tab(children=(VBox(children=(HBox(children=(Dropdown(layout=Layout(width='80px'), options=('data', 'exec'), va…"
       ]
      },
      "metadata": {},
diff --git a/notebooks/example.json b/notebooks/example.json
index 0212305d..b57d7d48 100644
--- a/notebooks/example.json
+++ b/notebooks/example.json
@@ -6,6 +6,90 @@
             "flow": {
                 "algorithm mode": "data",
                 "nodes": [
+                    {
+                        "identifier": "BulkStructure_Node",
+                        "version": null,
+                        "state data": "gAR9lC4=",
+                        "additional data": {},
+                        "inputs": [
+                            {
+                                "type": "data",
+                                "label": "element",
+                                "GID": 9,
+                                "val": "gASVBgAAAAAAAACMAkZllC4=",
+                                "dtype": "DType.String",
+                                "dtype state": "gASVbgAAAAAAAAB9lCiMB2RlZmF1bHSUjAJGZZSMA3ZhbJRoAowDZG9jlIwAlIwGYm91bmRzlE6MDXZhbGlkX2NsYXNzZXOUXZSMCGJ1aWx0aW5zlIwDc3RylJOUYYwKYWxsb3dfbm9uZZSJjARzaXpllIwBbZR1Lg=="
+                            },
+                            {
+                                "type": "data",
+                                "label": "crystal_structure",
+                                "GID": 10,
+                                "val": "gAROLg==",
+                                "dtype": "DType.Choice",
+                                "dtype state": "gASVtwAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjAphbGxvd19ub25llIiMBWl0ZW1zlF2UKE6MAnNjlIwDZmNjlIwDYmNjlIwDaGNwlIwHZGlhbW9uZJSMCnppbmNibGVuZGWUjAhyb2Nrc2FsdJSMDmNlc2l1bWNobG9yaWRllIwIZmx1b3JpdGWUjAh3dXJ0eml0ZZRldS4="
+                            },
+                            {
+                                "type": "data",
+                                "label": "a",
+                                "GID": 11,
+                                "val": "gAROLg==",
+                                "dtype": "DType.Float",
+                                "dtype state": "gASVbQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjAhidWlsdGluc5SMBWZsb2F0lJOUYYwKYWxsb3dfbm9uZZSIjAhkZWNpbWFsc5RLCnUu"
+                            },
+                            {
+                                "type": "data",
+                                "label": "c",
+                                "GID": 12,
+                                "val": "gAROLg==",
+                                "dtype": "DType.Float",
+                                "dtype state": "gASVbQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjAhidWlsdGluc5SMBWZsb2F0lJOUYYwKYWxsb3dfbm9uZZSIjAhkZWNpbWFsc5RLCnUu"
+                            },
+                            {
+                                "type": "data",
+                                "label": "c_over_a",
+                                "GID": 13,
+                                "val": "gAROLg==",
+                                "dtype": "DType.Float",
+                                "dtype state": "gASVbQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjAhidWlsdGluc5SMBWZsb2F0lJOUYYwKYWxsb3dfbm9uZZSIjAhkZWNpbWFsc5RLCnUu"
+                            },
+                            {
+                                "type": "data",
+                                "label": "u",
+                                "GID": 14,
+                                "val": "gAROLg==",
+                                "dtype": "DType.Float",
+                                "dtype state": "gASVbQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjAhidWlsdGluc5SMBWZsb2F0lJOUYYwKYWxsb3dfbm9uZZSIjAhkZWNpbWFsc5RLCnUu"
+                            },
+                            {
+                                "type": "data",
+                                "label": "orthorhombic",
+                                "GID": 15,
+                                "val": "gASJLg==",
+                                "dtype": "DType.Boolean",
+                                "dtype state": "gASVXwAAAAAAAAB9lCiMB2RlZmF1bHSUiYwDdmFslImMA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjAhidWlsdGluc5SMBGJvb2yUk5RhjAphbGxvd19ub25llIl1Lg=="
+                            },
+                            {
+                                "type": "data",
+                                "label": "cubic",
+                                "GID": 16,
+                                "val": "gASJLg==",
+                                "dtype": "DType.Boolean",
+                                "dtype state": "gASVXwAAAAAAAAB9lCiMB2RlZmF1bHSUiYwDdmFslImMA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjAhidWlsdGluc5SMBGJvb2yUk5RhjAphbGxvd19ub25llIl1Lg=="
+                            }
+                        ],
+                        "outputs": [
+                            {
+                                "type": "data",
+                                "label": "structure",
+                                "GID": 17,
+                                "dtype": "DType.Data",
+                                "dtype state": "gASVjwAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjCxweWlyb25fYXRvbWlzdGljcy5hdG9taXN0aWNzLnN0cnVjdHVyZS5hdG9tc5SMBUF0b21zlJOUYYwKYWxsb3dfbm9uZZSJjARzaXpllIwBbZR1Lg=="
+                            }
+                        ],
+                        "GID": 8,
+                        "pos x": 244.64050415353768,
+                        "pos y": 265.9340659340659
+                    },
                     {
                         "identifier": "Project_Node",
                         "version": null,
@@ -15,22 +99,24 @@
                             {
                                 "type": "data",
                                 "label": "name",
-                                "GID": 8,
-                                "val": "gASVCwAAAAAAAACMB2V4YW1wbGWULg==",
-                                "dtype": "DType.Char",
-                                "dtype state": "gASVLgAAAAAAAAB9lCiMB2RlZmF1bHSUjAEulIwDdmFslGgCjANkb2OUjACUjAZib3VuZHOUTnUu"
+                                "GID": 19,
+                                "val": "gASVBQAAAAAAAACMAS6ULg==",
+                                "dtype": "DType.String",
+                                "dtype state": "gASVbQAAAAAAAAB9lCiMB2RlZmF1bHSUjAEulIwDdmFslGgCjANkb2OUjACUjAZib3VuZHOUTowNdmFsaWRfY2xhc3Nlc5RdlIwIYnVpbHRpbnOUjANzdHKUk5RhjAphbGxvd19ub25llImMBHNpemWUjAFtlHUu"
                             }
                         ],
                         "outputs": [
                             {
                                 "type": "data",
                                 "label": "project",
-                                "GID": 9
+                                "GID": 20,
+                                "dtype": "DType.Data",
+                                "dtype state": "gASVfgAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjBlweWlyb25fYXRvbWlzdGljcy5wcm9qZWN0lIwHUHJvamVjdJSTlGGMCmFsbG93X25vbmWUiYwEc2l6ZZSMAW2UdS4="
                             }
                         ],
-                        "GID": 7,
-                        "pos x": 638.5876791254248,
-                        "pos y": 106.51773049645391
+                        "GID": 18,
+                        "pos x": 513.03351475222,
+                        "pos y": 127.47252747252747
                     },
                     {
                         "identifier": "Plot3d_Node",
@@ -41,26 +127,30 @@
                             {
                                 "type": "data",
                                 "label": "structure",
-                                "GID": 16,
+                                "GID": 22,
                                 "dtype": "DType.Data",
-                                "dtype state": "gASVNQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjARzaXpllIwBbZR1Lg=="
+                                "dtype state": "gASVjwAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjCxweWlyb25fYXRvbWlzdGljcy5hdG9taXN0aWNzLnN0cnVjdHVyZS5hdG9tc5SMBUF0b21zlJOUYYwKYWxsb3dfbm9uZZSJjARzaXpllIwBbZR1Lg=="
                             }
                         ],
                         "outputs": [
                             {
                                 "type": "data",
                                 "label": "plot3d",
-                                "GID": 17
+                                "GID": 23,
+                                "dtype": "DType.Data",
+                                "dtype state": "gASVdQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjA5uZ2x2aWV3LndpZGdldJSMCU5HTFdpZGdldJSTlGGMCmFsbG93X25vbmWUiYwEc2l6ZZSMAW2UdS4="
                             },
                             {
                                 "type": "data",
                                 "label": "structure",
-                                "GID": 18
+                                "GID": 24,
+                                "dtype": "DType.Data",
+                                "dtype state": "gASVjwAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjCxweWlyb25fYXRvbWlzdGljcy5hdG9taXN0aWNzLnN0cnVjdHVyZS5hdG9tc5SMBUF0b21zlJOUYYwKYWxsb3dfbm9uZZSJjARzaXpllIwBbZR1Lg=="
                             }
                         ],
-                        "GID": 15,
-                        "pos x": 775.7266719437439,
-                        "pos y": 505.0397877984085
+                        "GID": 21,
+                        "pos x": 693.4288169578917,
+                        "pos y": 569.2307692307693
                     },
                     {
                         "identifier": "Lammps_Node",
@@ -71,61 +161,63 @@
                             {
                                 "type": "exec",
                                 "label": "run",
-                                "GID": 20,
+                                "GID": 26,
                                 "val": "gAROLg=="
                             },
                             {
                                 "type": "exec",
                                 "label": "remove",
-                                "GID": 21,
+                                "GID": 27,
                                 "val": "gAROLg=="
                             },
                             {
                                 "type": "data",
                                 "label": "project",
-                                "GID": 22,
+                                "GID": 28,
                                 "dtype": "DType.Data",
-                                "dtype state": "gASVNQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjARzaXpllIwBbZR1Lg=="
+                                "dtype state": "gASVfgAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjBlweWlyb25fYXRvbWlzdGljcy5wcm9qZWN0lIwHUHJvamVjdJSTlGGMCmFsbG93X25vbmWUiYwEc2l6ZZSMAW2UdS4="
                             },
                             {
                                 "type": "data",
                                 "label": "name",
-                                "GID": 23,
+                                "GID": 29,
                                 "val": "gASVBwAAAAAAAACMA2pvYpQu",
-                                "dtype": "DType.Char",
-                                "dtype state": "gASVMAAAAAAAAAB9lCiMB2RlZmF1bHSUjANqb2KUjAN2YWyUaAKMA2RvY5SMAJSMBmJvdW5kc5ROdS4="
+                                "dtype": "DType.String",
+                                "dtype state": "gASVbwAAAAAAAAB9lCiMB2RlZmF1bHSUjANqb2KUjAN2YWyUaAKMA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjAhidWlsdGluc5SMA3N0cpSTlGGMCmFsbG93X25vbmWUiYwEc2l6ZZSMAW2UdS4="
                             },
                             {
                                 "type": "data",
                                 "label": "structure",
-                                "GID": 24,
+                                "GID": 30,
                                 "dtype": "DType.Data",
-                                "dtype state": "gASVNQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjARzaXpllIwBbZR1Lg=="
+                                "dtype state": "gASVjwAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjCxweWlyb25fYXRvbWlzdGljcy5hdG9taXN0aWNzLnN0cnVjdHVyZS5hdG9tc5SMBUF0b21zlJOUYYwKYWxsb3dfbm9uZZSJjARzaXpllIwBbZR1Lg=="
                             },
                             {
                                 "type": "data",
                                 "label": "potential",
-                                "GID": 25,
-                                "val": "gASVKwAAAAAAAACMJzE5OTUtLUFuZ2Vsby1KLUUtLU5pLUFsLUgtLUxBTU1QUy0taXByMZQu",
+                                "GID": 31,
+                                "val": "gASVJwAAAAAAAACMIzE5OTctLUFja2xhbmQtRy1KLS1GZS0tTEFNTVBTLS1pcHIxlC4=",
                                 "dtype": "DType.Choice",
-                                "dtype state": "gASVWxkAAAAAAAB9lCiMB2RlZmF1bHSUjBNTZXQgc3RydWN0dXJlIGZpcnN0lIwDdmFslGgCjANkb2OUjACUjAZib3VuZHOUTowFaXRlbXOUXZQojCcxOTk1LS1BbmdlbG8tSi1FLS1OaS1BbC1ILS1MQU1NUFMtLWlwcjGUjCYxOTk2LS1GYXJrYXMtRC0tTmItVGktQWwtLUxBTU1QUy0taXByMZSMIjE5OTctLUxpdS1YLVktLUFsLU1nLS1MQU1NUFMtLWlwcjGUjCIxOTk4LS1MaXUtWC1ZLS1BbC1NZy0tTEFNTVBTLS1pcHIxlIwiMTk5OS0tTGl1LVgtWS0tQWwtQ3UtLUxBTU1QUy0taXByMZSMIDE5OTktLU1pc2hpbi1ZLS1BbC0tTEFNTVBTLS1pcHIxlIwiMjAwMC0tTGFuZGEtQS0tQWwtUGItLUxBTU1QUy0taXByMZSMJDIwMDAtLVN0dXJnZW9uLUotQi0tQWwtLUxBTU1QUy0taXByMZSMIzIwMDItLU1pc2hpbi1ZLS1OaS1BbC0tTEFNTVBTLS1pcHIxlIwfMjAwMy0tTGVlLUItSi0tQWwtLUxBTU1QUy0taXByMZSMIDIwMDMtLVpvcGUtUi1SLS1BbC0tTEFNTVBTLS1pcHIxlIwjMjAwMy0tWm9wZS1SLVItLVRpLUFsLS1MQU1NUFMtLWlwcjGUjB8yMDA0LS1MaXUtWC1ZLS1BbC0tTEFNTVBTLS1pcHIxlIwjMjAwNC0tTWlzaGluLVktLU5pLUFsLS1MQU1NUFMtLWlwcjGUjCMyMDA0LS1NaXNoaW4tWS0tTmktQWwtLUxBTU1QUy0taXByMpSMIDIwMDQtLVpob3UtWC1XLS1BbC0tTEFNTVBTLS1pcHIylIwnMjAwNS0tTWVuZGVsZXYtTS1JLS1BbC1GZS0tTEFNTVBTLS1pcHIxlIwkMjAwNy0tU2lsdmEtQS1DLS1BbC1OaS0tTEFNTVBTLS1pcHIxlIwkMjAwOC0tTWVuZGVsZXYtTS1JLS1BbC0tTEFNTVBTLS1pcHIxlIwiMjAwOS0tS2ltLVktTS0tTWctQWwtLUxBTU1QUy0taXByMZSMJzIwMDktLU1lbmRlbGV2LU0tSS0tQWwtTWctLUxBTU1QUy0taXByMZSMKDIwMDktLVB1cmphLVB1bi1HLVAtLU5pLUFsLS1MQU1NUFMtLWlwcjGUjCcyMDA5LS1aaGFraG92c2tpaS1WLVYtLUFsLS1MQU1NUFMtLWlwcjKUjCAyMDEwLS1MZWUtRS0tRmUtQWwtLUxBTU1QUy0taXByMZSMMDIwMTAtLU1lbmRlbGV2LU0tSS0tZmljdGlvbmFsLUFsLTEtLUxBTU1QUy0taXByMZSMMDIwMTAtLU1lbmRlbGV2LU0tSS0tZmljdGlvbmFsLUFsLTItLUxBTU1QUy0taXByMZSMMDIwMTAtLU1lbmRlbGV2LU0tSS0tZmljdGlvbmFsLUFsLTMtLUxBTU1QUy0taXByMZSMITIwMTAtLVdpbmV5LUotTS0tQWwtLUxBTU1QUy0taXByMZSMJDIwMTEtLUFwb3N0b2wtRi0tQWwtQ3UtLUxBTU1QUy0taXByMZSMIDIwMTEtLUtvLVctUy0tQWwtSC0tTEFNTVBTLS1pcHIxlIwjMjAxMi0tRG9uZy1XLVAtLUNvLUFsLS1MQU1NUFMtLWlwcjGUjC0yMDEyLS1KZWxpbmVrLUItLUFsLVNpLU1nLUN1LUZlLS1MQU1NUFMtLWlwcjKUjCYyMDEyLS1TY2hvcGYtRC0tQWwtTW4tUGQtLUxBTU1QUy0taXByMZSMIjIwMTMtLVNoaW0tSi1ILS1WLUFsLS1MQU1NUFMtLWlwcjGUjCQyMDEzLS1TaGltLUotSC0tVi1BbC1ILS1MQU1NUFMtLWlwcjGUjCMyMDE1LS1DaG91ZGhhcnktSy0tQWwtLUxBTU1QUy0taXByMZSMJTIwMTUtLUNob3VkaGFyeS1LLS1BbC1PLS1MQU1NUFMtLWlwcjGUjCUyMDE1LS1LaW0tWS1LLS1OaS1BbC1Dby0tTEFNTVBTLS1pcHIxlIwiMjAxNS0tS3VtYXItQS0tQWwtTmktLUxBTU1QUy0taXByMZSMJDIwMTUtLUt1bWFyLUEtLUFsLU5pLU8tLUxBTU1QUy0taXByMZSMJzIwMTUtLU1lbmRlbGV2LU0tSS0tQWwtU20tLUxBTU1QUy0taXByMZSMIzIwMTUtLVBhc2N1ZXQtTS1JLS1BbC0tTEFNTVBTLS1pcHIxlIwlMjAxNS0tUGFzY3VldC1NLUktLUFsLVUtLUxBTU1QUy0taXByMpSMKDIwMTUtLVB1cmphLVB1bi1HLVAtLUFsLUNvLS1MQU1NUFMtLWlwcjKUjCsyMDE1LS1QdXJqYS1QdW4tRy1QLS1OaS1BbC1Dby0tTEFNTVBTLS1pcHIylIwiMjAxNi0tS2ltLVktSy0tQWwtVGktLUxBTU1QUy0taXByMZSMIzIwMTYtLVpob3UtWC1XLS1BbC1DdS0tTEFNTVBTLS1pcHIylIweMjAxNy0tQm90dS1WLS1BbC0tTEFNTVBTLS1pcHIxlIwiMjAxNy0tS2ltLUotUy0tUHQtQWwtLUxBTU1QUy0taXByMZSMJTIwMTctLUtpbS1ZLUstLU5pLUFsLVRpLS1MQU1NUFMtLWlwcjGUjCgyMDE4LS1EaWNrZWwtRC1FLS1NZy1BbC1abi0tTEFNTVBTLS1pcHIxlIwkMjAxOC0tSmVvbmctRy1VLS1QZC1BbC0tTEFNTVBTLS1pcHIxlIwlMjAxOC0tWmhvdS1YLVctLUFsLUN1LUgtLUxBTU1QUy0taXByMZSMLDIwMjAtLUZhcmthcy1ELS1GZS1OaS1Dci1Dby1BbC0tTEFNTVBTLS1pcHIxlIwoMjAyMC0tU3Rhcmlrb3YtUy0tU2ktQXUtQWwtLUxBTU1QUy0taXByMZSMKDIwMjAtLVN0YXJpa292LVMtLVNpLUF1LUFsLS1MQU1NUFMtLWlwcjKUjEJFQU1fQ3ViaWNOYXR1cmFsU3BsaW5lX0VyY29sZXNzaUFkYW1zXzE5OTRfQWxfX01PXzgwMDUwOTQ1ODcxMl8wMDKUjDxFQU1fRHluYW1vX0FuZ2Vsb01vb2R5QmFza2VzXzE5OTVfTmlBbEhfX01PXzQxODk3ODIzNzA1OF8wMDWUjC9FQU1fRHluYW1vX0NhaVllXzE5OTZfQWxDdV9fTU9fOTQyNTUxMDQwMDQ3XzAwNZSMNkVBTV9EeW5hbW9fRXJjb2xlc3NpQWRhbXNfMTk5NF9BbF9fTU9fMTIzNjI5NDIyMDQ1XzAwNZSMN0VBTV9EeW5hbW9fRmFya2FzSm9uZXNfMTk5Nl9OYlRpQWxfX01PXzA0MjY5MTM2Nzc4MF8wMDCUjDxFQU1fRHluYW1vX0phY29ic2VuTm9yc2tvdlB1c2thXzE5ODdfQWxfX01PXzQxMTY5MjEzMzM2Nl8wMDCUjD1FQU1fRHluYW1vX0xhbmRhV3luYmxhdHRTaWVnZWxfMjAwMF9BbFBiX19NT182OTkxMzczOTYzODFfMDA1lIwyRUFNX0R5bmFtb19MaXVBZGFtc18xOTk4X0FsTWdfX01PXzAxOTg3MzcxNTc4Nl8wMDCUjDlFQU1fRHluYW1vX0xpdUVyY29sZXNzaUFkYW1zXzIwMDRfQWxfX01PXzA1MTE1NzY3MTUwNV8wMDCUjDdFQU1fRHluYW1vX0xpdUxpdUJvcnVja2lfMTk5OV9BbEN1X19NT18wMjA4NTEwNjk1NzJfMDAwlIw7RUFNX0R5bmFtb19MaXVPaG90bmlja3lBZGFtc18xOTk3X0FsTWdfX01PXzU1OTg3MDYxMzU0OV8wMDCUjDxFQU1fRHluYW1vX01lbmRlbGV2QXN0YVJhaG1hbl8yMDA5X0FsTWdfX01PXzY1ODI3ODU0OTc4NF8wMDWUjDhFQU1fRHluYW1vX01lbmRlbGV2RmFuZ1llXzIwMTVfQWxTbV9fTU9fMzM4NjAwMjAwNzM5XzAwMJSMPEVBTV9EeW5hbW9fTWVuZGVsZXZLcmFtZXJCZWNrZXJfMjAwOF9BbF9fTU9fMTA2OTY5NzAxMDIzXzAwNZSMQkVBTV9EeW5hbW9fTWVuZGVsZXZTcm9sb3ZpdHpBY2tsYW5kXzIwMDVfQWxGZV9fTU9fNTc3NDUzODkxOTQxXzAwNZSMOEVBTV9EeW5hbW9fTWlzaGluRmFya2FzTWVobF8xOTk5X0FsX19NT182NTE4MDE0ODY2NzlfMDA1lIxHRUFNX0R5bmFtb19NaXNoaW5NZWhsUGFwYWNvbnN0YW50b3BvdWxvc18yMDAyX05pQWxfX01PXzEwOTkzMzU2MTUwN18wMDWUjDBFQU1fRHluYW1vX01pc2hpbl8yMDA0X05pQWxfX01PXzEwMTIxNDMxMDY4OV8wMDWUjDNFQU1fRHluYW1vX1B1bk1pc2hpbl8yMDA5X05pQWxfX01PXzc1MTM1NDQwMzc5MV8wMDWUjDpFQU1fRHluYW1vX1B1bllhbWFrb3ZNaXNoaW5fMjAxM19BbENvX19NT182Nzg5NTI2MTI0MTNfMDAwlIw8RUFNX0R5bmFtb19QdW5ZYW1ha292TWlzaGluXzIwMTNfTmlBbENvX19NT184MjY1OTEzNTk1MDhfMDAwlIw/RUFNX0R5bmFtb19TY2hvcGZCcm9tbWVyRnJpZ2FuXzIwMTJfQWxNblBkX19NT18xMzc1NzI4MTc4NDJfMDAwlIw1RUFNX0R5bmFtb19TdHVyZ2VvbkxhaXJkXzIwMDBfQWxfX01PXzEyMDgwODgwNTU0MV8wMDWUjDZFQU1fRHluYW1vX1ZhaWxoZUZhcmthc18xOTk3X0NvQWxfX01PXzI4NDk2MzE3OTQ5OF8wMDWUjDhFQU1fRHluYW1vX1dpbmV5S3Vib3RhR3VwdGFfMjAxMF9BbF9fTU9fMTQ5MzE2ODY1NjA4XzAwNZSMMkVBTV9EeW5hbW9fWmhha2hvdnNreV8yMDA5X0FsX19NT181MTk2MTM4OTMxOTZfMDAwlIxJRUFNX0R5bmFtb19aaG91Sm9obnNvbldhZGxleV8yMDA0TklTVHJldGFidWxhdGlvbl9BbF9fTU9fMDYwNTY3ODY4NTU4XzAwMJSMOUVBTV9EeW5hbW9fWmhvdUpvaG5zb25XYWRsZXlfMjAwNF9BbF9fTU9fMTMxNjUwMjYxNTEwXzAwNZSMOUVBTV9EeW5hbW9fWmhvdVdhZGxleUpvaG5zb25fMjAwMV9BbF9fTU9fMDQ5MjQzNDk4NTU1XzAwMJSMMkVBTV9EeW5hbW9fWm9wZU1pc2hpbl8yMDAzX0FsX19NT182NjQ0NzAxMTQzMTFfMDA1lIw0RUFNX0R5bmFtb19ab3BlTWlzaGluXzIwMDNfVGlBbF9fTU9fMTE3NjU2Nzg2NzYwXzAwNZSML0VBTV9FcmNvbGVzc2lBZGFtc18xOTk0X0FsX19NT18zMjQ1MDc1MzYzNDVfMDAzlIw4RUFNX0lNRF9Ccm9tbWVyR2FlaGxlcl8yMDA2QV9BbE5pQ29fX01PXzEyMjcwMzcwMDIyM18wMDOUjDhFQU1fSU1EX0Jyb21tZXJHYWVobGVyXzIwMDZCX0FsTmlDb19fTU9fMTI4MDM3NDg1Mjc2XzAwM5SMPEVBTV9JTURfU2Nob3BmQnJvbW1lckZyaWdhbl8yMDEyX0FsTW5QZF9fTU9fODc4NzEyOTc4MDYyXzAwM5SMREVBTV9RdWludGljQ2xhbXBlZFNwbGluZV9FcmNvbGVzc2lBZGFtc18xOTk0X0FsX19NT180NTAwOTM3MjczOTZfMDAylIxERUFNX1F1aW50aWNIZXJtaXRlU3BsaW5lX0VyY29sZXNzaUFkYW1zXzE5OTRfQWxfX01PXzc4MTEzODY3MTg2M18wMDKUjFFFTVRfQXNhcF9TdGFuZGFyZF9KYWNvYnNlblN0b2x0emVOb3Jza292XzE5OTZfQWxBZ0F1Q3VOaVBkUHRfX01PXzExNTMxNjc1MDk4Nl8wMDGUjEVFTVRfQXNhcF9TdGFuZGFyZF9KYWNvYnNlblN0b2x0emVOb3Jza292XzE5OTZfQWxfX01PXzYyMzM3NjEyNDg2Ml8wMDGUjEdNRUFNX0xBTU1QU19BbG15cmFzU2FuZ2lvdmFubmlTYXJha2lub3NfMjAxOV9OQWxUaV9fTU9fOTU4Mzk1MTkwNjI3XzAwMJSMP01FQU1fTEFNTVBTX0Nvc3RhQWdyZW5DbGF2YWd1ZXJhXzIwMDdfQWxOaV9fTU9fMTMxNjQyNzY4Mjg4XzAwMJSMNE1FQU1fTEFNTVBTX0RvbmdLaW1Lb18yMDEyX0NvQWxfX01PXzA5OTcxNjQxNjIxNl8wMDCUjEdNRUFNX0xBTU1QU19KZWxpbmVrR3JvaEhvcnN0ZW1leWVyXzIwMTJfQWxTaU1nQ3VGZV9fTU9fMjYyNTE5NTIwNjc4XzAwMJSMNk1FQU1fTEFNTVBTX0plb25nUGFya0RvXzIwMThfUGRBbF9fTU9fNjE2NDgyMzU4ODA3XzAwMJSMN01FQU1fTEFNTVBTX0tpbUp1bmdMZWVfMjAxNV9OaUFsQ29fX01PXzg3NjY4NzE2NjUxOV8wMDCUjDVNRUFNX0xBTU1QU19LaW1LaW1KdW5nXzIwMTZfQWxUaV9fTU9fNjE4MTMzNzYzMzc1XzAwMJSMN01FQU1fTEFNTVBTX0tpbUtpbUp1bmdfMjAxN19OaUFsVGlfX01PXzQ3ODk2NzI1NTQzNV8wMDCUjDRNRUFNX0xBTU1QU19LaW1LaW1MZWVfMjAwOV9BbE1nX19NT18wNTg1MzcwODczODRfMDAwlIw0TUVBTV9MQU1NUFNfS2ltU2VvbEppXzIwMTdfUHRBbF9fTU9fNzkzMTQxMDM3NzA2XzAwMJSMM01FQU1fTEFNTVBTX0tvU2hpbUxlZV8yMDExX0FsSF9fTU9fMTI3ODQ3MDgwNzUxXzAwMJSMMU1FQU1fTEFNTVBTX0xlZUxlZV8yMDEwX0ZlQWxfX01PXzMzMjIxMTUyMjA1MF8wMDCUjDpNRUFNX0xBTU1QU19QYXNjdWV0RmVybmFuZGV6XzIwMTVfQWxVX19NT181OTYzMDA2NzM5MTdfMDAwlIw5TUVBTV9MQU1NUFNfUGFzY3VldEZlcm5hbmRlel8yMDE1X0FsX19NT18zMTU4MjA5NzQxNDlfMDAwlIw0TUVBTV9MQU1NUFNfU2hpbUtvS2ltXzIwMTNfQWxWSF9fTU9fMzQ0NzI0MTQ1MzM5XzAwMJSMRE1vcnNlX1NoaWZ0ZWRfR2lyaWZhbGNvV2VpemVyXzE5NTlIaWdoQ3V0b2ZmX0FsX19NT18xNDAxNzU3NDg2MjZfMDA0lIxDTW9yc2VfU2hpZnRlZF9HaXJpZmFsY29XZWl6ZXJfMTk1OUxvd0N1dG9mZl9BbF9fTU9fNDExODk4OTUzNjYxXzAwNJSMQ01vcnNlX1NoaWZ0ZWRfR2lyaWZhbGNvV2VpemVyXzE5NTlNZWRDdXRvZmZfQWxfX01PXzI3OTU0NDc0NjA5N18wMDSUjDtTaW1fTEFNTVBTX0FEUF9BcG9zdG9sTWlzaGluXzIwMTFfQWxDdV9fU01fNjY3Njk2NzYzNTYxXzAwMJSMSlNpbV9MQU1NUFNfQURQX1N0YXJpa292R29yZGVldkx5c29nb3Jza2l5XzIwMjBfU2lBdUFsX19TTV8xMTM4NDM4MzA2MDJfMDAwlIw9U2ltX0xBTU1QU19BR05JX0JvdHVCYXRyYUNoYXBtYW5fMjAxN19BbF9fU01fNjY2MTgzNjM2ODk2XzAwMJSMPFNpbV9MQU1NUFNfQk9QX1pob3VXYXJkRm9zdGVyXzIwMTZfQWxDdV9fU01fNTY2Mzk5MjU4Mjc5XzAwMJSMS1NpbV9MQU1NUFNfTUVBTV9BbG15cmFzU2FuZ2lvdmFubmlTYXJha2lub3NfMjAxOV9OQWxUaV9fU01fODcxNzk1MjQ5MDUyXzAwMJSMS1NpbV9MQU1NUFNfTUVBTV9KZWxpbmVrR3JvaEhvcnN0ZW1leWVyXzIwMTJfQWxTaU1nQ3VGZV9fU01fNjU2NTE3MzUyNDg1XzAwMJSMPlNpbV9MQU1NUFNfTUVBTV9QYXNjdWV0RmVybmFuZGV6XzIwMTVfQWxVX19TTV83MjE5MzAzOTEwMDNfMDAwlIw9U2ltX0xBTU1QU19NRUFNX1Bhc2N1ZXRGZXJuYW5kZXpfMjAxNV9BbF9fU01fODExNTg4OTU3MTg3XzAwMJSMRVNpbV9MQU1NUFNfU01UQlFfU2FsbGVzUG9saXRhbm9BbXphbGxhZ18yMDE2X0FsT19fU01fODUzOTY3MzU1OTc2XzAwMJSMRFNpbV9MQU1NUFNfU01UQlFfU2FsbGVzUG9saXRhbm9BbXphbGxhZ18yMDE2X0FsX19TTV80MDQwOTc2MzM5MjRfMDAwlGV1Lg=="
+                                "dtype state": "gASVYxcAAAAAAAB9lCiMB2RlZmF1bHSUjBNTZXQgc3RydWN0dXJlIGZpcnN0lIwDdmFslGgCjANkb2OUjACUjAZib3VuZHOUTowNdmFsaWRfY2xhc3Nlc5RdlIwIYnVpbHRpbnOUjANzdHKUk5RhjAphbGxvd19ub25llImMBWl0ZW1zlF2UKIwjMTk5Ny0tQWNrbGFuZC1HLUotLUZlLS1MQU1NUFMtLWlwcjGUjB8xOTk4LS1NZXllci1SLS1GZS0tTEFNTVBTLS1pcHIxlIwfMjAwMS0tTGVlLUItSi0tRmUtLUxBTU1QUy0taXByMZSMIjIwMDEtLUxlZS1CLUotLUZlLUNyLS1MQU1NUFMtLWlwcjGUjCYyMDAzLS1NZW5kZWxldi1NLUktLUZlLTItLUxBTU1QUy0taXByM5SMJjIwMDMtLU1lbmRlbGV2LU0tSS0tRmUtNS0tTEFNTVBTLS1pcHIxlIwlMjAwNC0tQWNrbGFuZC1HLUotLUZlLVAtLUxBTU1QUy0taXByMZSMIDIwMDQtLVpob3UtWC1XLS1GZS0tTEFNTVBTLS1pcHIylIwiMjAwNS0tTGVlLUItSi0tRmUtQ3UtLUxBTU1QUy0taXByMZSMJzIwMDUtLU1lbmRlbGV2LU0tSS0tQWwtRmUtLUxBTU1QUy0taXByMZSMITIwMDYtLUNoYW1hdGktSC0tRmUtLUxBTU1QUy0taXByMZSMIDIwMDYtLUtpbS1KLS1GZS1QdC0tTEFNTVBTLS1pcHIxlIwhMjAwNi0tTGVlLUItSi0tRmUtQy0tTEFNTVBTLS1pcHIxlIwhMjAwNi0tTGVlLUItSi0tRmUtTi0tTEFNTVBTLS1pcHIxlIwhMjAwNy0tTGVlLUItSi0tRmUtSC0tTEFNTVBTLS1pcHIxlIwmMjAwNy0tTWVuZGVsZXYtTS1JLS1WLUZlLS1MQU1NUFMtLWlwcjGUjCUyMDA4LS1IZXBidXJuLUQtSi0tRmUtQy0tTEFNTVBTLS1pcHIxlIwfMjAwOC0tU2EtSS0tRmUtTmItLUxBTU1QUy0taXByMZSMHzIwMDgtLVNhLUktLUZlLVRpLS1MQU1NUFMtLWlwcjGUjCUyMDA5LS1Cb25ueS1HLS1GZS1DdS1OaS0tTEFNTVBTLS1pcHIxlIwiMjAwOS0tQm9ubnktRy0tRmUtTmktLUxBTU1QUy0taXByMZSMJDIwMDktLUtpbS1ILUstLUZlLVRpLUMtLUxBTU1QUy0taXByMpSMIjIwMDktLUtpbS1ZLU0tLUZlLU1uLS1MQU1NUFMtLWlwcjGUjCQyMDA5LS1PbHNzb24tUC1BLVQtLUZlLS1MQU1NUFMtLWlwcjGUjCYyMDA5LS1TdHVrb3dza2ktQS0tRmUtQ3ItLUxBTU1QUy0taXByMZSMJDIwMTAtLUtpbS1ILUstLUZlLU5iLUMtLUxBTU1QUy0taXByMZSMIDIwMTAtLUxlZS1FLS1GZS1BbC0tTEFNTVBTLS1pcHIxlIwhMjAxMC0tTWFsZXJiYS1MLS1GZS0tTEFNTVBTLS1pcHIxlIwiMjAxMS0tQm9ubnktRy0tRmUtQ3ItLUxBTU1QUy0taXByMpSMJTIwMTEtLUJvbm55LUctLUZlLU5pLUNyLS1MQU1NUFMtLWlwcjGUjCMyMDExLS1DaGllc2EtUy0tRmUtMzMtLUxBTU1QUy0taXByMZSMLTIwMTItLUplbGluZWstQi0tQWwtU2ktTWctQ3UtRmUtLUxBTU1QUy0taXByMpSMIDIwMTItLUtvLVctUy0tRmUtUC0tTEFNTVBTLS1pcHIxlIwiMjAxMi0tUHJvdmlsbGUtTC0tRmUtLUxBTU1QUy0taXByMZSMJDIwMTMtLUJvbm55LUctLUZlLUNyLVctLUxBTU1QUy0taXByMpSMJTIwMTMtLUJvbm55LUctLUZlLU5pLUNyLS1MQU1NUFMtLWlwcjGUjCEyMDEzLS1Cb25ueS1HLS1GZS1XLS1MQU1NUFMtLWlwcjGUjCoyMDEzLS1IZW5yaWtzc29uLUstTy1FLS1GZS1DLS1MQU1NUFMtLWlwcjGUjCgyMDE0LS1MaXlhbmFnZS1MLVMtSS0tRmUtQy0tTEFNTVBTLS1pcHIylIwfMjAxNS0tQXNhZGktRS0tRmUtLUxBTU1QUy0taXByMZSMKDIwMTctLUJlbGFuZC1MLUstLUZlLU5pLUNyLS1MQU1NUFMtLWlwcjGUjCMyMDE3LS1DaG9pLVctTS0tQ28tRmUtLUxBTU1QUy0taXByMZSMIjIwMTctLVd1LUMtLU5pLUNyLUZlLS1MQU1NUFMtLWlwcjGUjB8yMDE3LS1XdS1DLS1OaS1GZS0tTEFNTVBTLS1pcHIxlIwsMjAxOC0tQ2hvaS1XLU0tLUNvLU5pLUNyLUZlLU1uLS1MQU1NUFMtLWlwcjGUjCMyMDE4LS1FdGVzYW1pLVMtQS0tRmUtLUxBTU1QUy0taXByMZSMLDIwMTgtLUZhcmthcy1ELS1GZS1OaS1Dci1Dby1DdS0tTEFNTVBTLS1pcHIylIwkMjAxOC0tSmVvbmctRy1VLS1QZC1GZS0tTEFNTVBTLS1pcHIxlIwmMjAxOC0tWmhvdS1YLVctLUZlLU5pLUNyLS1MQU1NUFMtLWlwcjGUjCYyMDE4LS1aaG91LVgtVy0tRmUtTmktQ3ItLUxBTU1QUy0taXByMpSMJzIwMTktLUFzbGFtLUktLUZlLU1uLVNpLUMtLUxBTU1QUy0taXByMZSMJjIwMTktLUJ5Z2dtYXN0YXItSi0tRmUtTy0tTEFNTVBTLS1pcHIxlIwqMjAxOS0tTWVuZGVsZXYtTS1JLS1GZS1OaS1Dci0tTEFNTVBTLS1pcHIxlIwkMjAyMC0tQnlnZ21hc3Rhci1KLS1GZS0tTEFNTVBTLS1pcHIxlIwsMjAyMC0tRmFya2FzLUQtLUZlLU5pLUNyLUNvLUFsLS1MQU1NUFMtLWlwcjGUjB4yMDIwLS1Nb3JpLUgtLUZlLS1MQU1NUFMtLWlwcjGUjDpFQU1fRHluYW1vX0Fja2xhbmRCYWNvbkNhbGRlcl8xOTk3X0ZlX19NT18xNDI3OTk3MTc1MTZfMDA1lIxBRUFNX0R5bmFtb19BY2tsYW5kTWVuZGVsZXZTcm9sb3ZpdHpfMjAwNF9GZVBfX01PXzg4NDM0MzE0NjMxMF8wMDWUjDtFQU1fRHluYW1vX0Jvbm55Q2FzdGluQnVsbGVuc18yMDEzX0ZlV19fTU9fNzM3NTY3MjQyNjMxXzAwMJSMQEVBTV9EeW5hbW9fQm9ubnlDYXN0aW5UZXJlbnR5ZXZfMjAxM19GZU5pQ3JfX01PXzc2MzE5Nzk0MTAzOV8wMDCUjD9FQU1fRHluYW1vX0Jvbm55UGFzaWFub3RDYXN0aW5fMjAwOV9GZUN1TmlfX01PXzQ2OTM0Mzk3MzE3MV8wMDWUjD5FQU1fRHluYW1vX0Jvbm55UGFzaWFub3RNYWxlcmJhXzIwMDlfRmVOaV9fTU9fMjY3NzIxNDA4OTM0XzAwNZSMQUVBTV9EeW5hbW9fQ2hhbWF0aVBhcGFuaWNvbGFvdU1pc2hpbl8yMDA2X0ZlX19NT185NjA2OTk1MTM0MjRfMDAwlIw3RUFNX0R5bmFtb19IZXBidXJuQWNrbGFuZF8yMDA4X0ZlQ19fTU9fMTQzOTc3MTUyNzI4XzAwNZSMMEVBTV9EeW5hbW9fTWFyaW5pY2FfMjAwN19GZV9fTU9fNDY2ODA4ODc3MTMwXzAwMJSMMEVBTV9EeW5hbW9fTWFyaW5pY2FfMjAxMV9GZV9fTU9fMjU1MzE1NDA3OTEwXzAwMJSMPUVBTV9EeW5hbW9fTWVuZGVsZXZCb3Jvdmlrb3ZfMjAyMF9GZU5pQ3JfX01PXzkyMjM2MzM0MDU3MF8wMDCUjDdFQU1fRHluYW1vX01lbmRlbGV2SGFuU29uXzIwMDdfVkZlX19NT18yNDk3MDY4MTA1MjdfMDA1lIxGRUFNX0R5bmFtb19NZW5kZWxldkhhblNyb2xvdml0el8yMDAzUG90ZW50aWFsMl9GZV9fTU9fNzY5NTgyMzYzNDM5XzAwNZSMRkVBTV9EeW5hbW9fTWVuZGVsZXZIYW5Tcm9sb3ZpdHpfMjAwM1BvdGVudGlhbDVfRmVfX01PXzk0MjQyMDcwNjg1OF8wMDWUjDxFQU1fRHluYW1vX01lbmRlbGV2SGFuU3JvbG92aXR6XzIwMDNfRmVfX01PXzgwNzk5NzgyNjQ0OV8wMDCUjEJFQU1fRHluYW1vX01lbmRlbGV2U3JvbG92aXR6QWNrbGFuZF8yMDA1X0FsRmVfX01PXzU3NzQ1Mzg5MTk0MV8wMDWUjDBFQU1fRHluYW1vX01lbmRlbGV2XzIwMDNfRmVfX01PXzU0NjY3MzU0OTA4NV8wMDCUjElFQU1fRHluYW1vX1pob3VKb2huc29uV2FkbGV5XzIwMDROSVNUcmV0YWJ1bGF0aW9uX0ZlX19NT182ODEwODgyOTgyMDhfMDAwlIw5RUFNX0R5bmFtb19aaG91Sm9obnNvbldhZGxleV8yMDA0X0ZlX19NT182NTAyNzk5MDUyMzBfMDA1lIxGRUFNX01hZ25ldGljMkdRdWludGljX0NoaWVzYURlcmxldER1ZGFyZXZfMjAxMV9GZV9fTU9fMTQwNDQ0MzIxNjA3XzAwMpSMPEVBTV9NYWduZXRpY0N1YmljX0R1ZGFyZXZEZXJsZXRfMjAwNV9GZV9fTU9fMTM1MDM0MjI5MjgyXzAwMpSMQ0VBTV9NYWduZXRpY0N1YmljX01lbmRlbGV2SGFuU3JvbG92aXR6XzIwMDNfRmVfX01PXzg1NjI5NTk1MjQyNV8wMDKUjDxNRUFNX0xBTU1QU19Bc2FkaVphZWVtTm91cmFuaWFuXzIwMTVfRmVfX01PXzQ5MjMxMDg5ODc3OV8wMDCUjDtNRUFNX0xBTU1QU19DaG9pSm9Tb2huXzIwMThfQ29OaUNyRmVNbl9fTU9fMTE1NDU0NzQ3NTAzXzAwMJSMNk1FQU1fTEFNTVBTX0Nob2lLaW1TZW9sXzIwMTdfQ29GZV9fTU9fMTc5MTU4MjU3MTgwXzAwMJSMNU1FQU1fTEFNTVBTX0V0ZXNhbWlBc2FkaV8yMDE4X0ZlX19NT181NDk5MDAyODc0MjFfMDAwlIxHTUVBTV9MQU1NUFNfSmVsaW5la0dyb2hIb3JzdGVtZXllcl8yMDEyX0FsU2lNZ0N1RmVfX01PXzI2MjUxOTUyMDY3OF8wMDCUjDZNRUFNX0xBTU1QU19KZW9uZ1BhcmtEb18yMDE4X1BkRmVfX01PXzkyNDczNjYyMjIwM18wMDCUjDZNRUFNX0xBTU1QU19LaW1KdW5nTGVlXzIwMDlfRmVUaUNfX01PXzExMDExOTIwNDcyM18wMDCUjDZNRUFNX0xBTU1QU19LaW1KdW5nTGVlXzIwMTBfRmVOYkNfX01PXzA3MjY4OTcxODYxNl8wMDCUjDFNRUFNX0xBTU1QU19LaW1MZWVfMjAwNl9QdEZlX19NT18zNDMxNjgxMDE0OTBfMDAwlIw1TUVBTV9MQU1NUFNfS2ltU2hpbkxlZV8yMDA5X0ZlTW5fX01PXzA1ODczNTQwMDQ2Ml8wMDCUjDJNRUFNX0xBTU1QU19Lb0ppbUxlZV8yMDEyX0ZlUF9fTU9fMTc5NDIwMzYzOTQ0XzAwMJSMMU1FQU1fTEFNTVBTX0xlZUphbmdfMjAwN19GZUhfX01PXzA5NTYxMDk1MTk1N18wMDCUjDNNRUFNX0xBTU1QU19MZWVMZWVLaW1fMjAwNl9GZU5fX01PXzQzMjg2MTc2NjczOF8wMDCUjDFNRUFNX0xBTU1QU19MZWVMZWVfMjAxMF9GZUFsX19NT18zMzIyMTE1MjIwNTBfMDAwlIw3TUVBTV9MQU1NUFNfTGVlV2lydGhTaGltXzIwMDVfRmVDdV9fTU9fMDYzNjI2MDY1NDM3XzAwMJSMLU1FQU1fTEFNTVBTX0xlZV8yMDA2X0ZlQ19fTU9fODU2OTU2MTc4NjY5XzAwMJSMOk1FQU1fTEFNTVBTX0xpeWFuYWdlS2ltSG91emVfMjAxNF9GZUNfX01PXzA3NTI3OTgwMDE5NV8wMDCUjDBNRUFNX0xBTU1QU19TYUxlZV8yMDA4X0ZlVGlfX01PXzI2MDU0Njk2Nzc5M18wMDCUjDBNRUFNX0xBTU1QU19TYUxlZV8yMDA4X05iRmVfX01PXzE2MjAzNjE0MTI2MV8wMDCUjDRNRUFNX0xBTU1QU19XdUxlZVN1XzIwMTdfTmlDckZlX19NT185MTI2MzYxMDcxMDhfMDAwlIwyTUVBTV9MQU1NUFNfV3VMZWVTdV8yMDE3X05pRmVfX01PXzMyMTIzMzE3NjQ5OF8wMDCUjDFNSl9Nb3JyaXNBZ2FMZXZhc2hvdl8yMDA4X0ZlX19NT184NTcyODI3NTQzMDdfMDAzlIxETW9yc2VfU2hpZnRlZF9HaXJpZmFsY29XZWl6ZXJfMTk1OUhpZ2hDdXRvZmZfRmVfX01PXzE0NzYwMzEyODQzN18wMDSUjENNb3JzZV9TaGlmdGVkX0dpcmlmYWxjb1dlaXplcl8xOTU5TG93Q3V0b2ZmX0ZlX19NT18zMzEyODU0OTU2MTdfMDA0lIxDTW9yc2VfU2hpZnRlZF9HaXJpZmFsY29XZWl6ZXJfMTk1OU1lZEN1dG9mZl9GZV9fTU9fOTg0MzU4MzQ0MTk2XzAwNJSMPVRlcnNvZmZfTEFNTVBTX011ZWxsZXJFcmhhcnRBbGJlXzIwMDdfRmVfX01PXzEzNzk2NDMxMDcwMl8wMDOUjEVTaW1fTEFNTVBTX0VBTUNEX1N0dWtvd3NraVNhZGlnaEVyaGFydF8yMDA5X0ZlQ3JfX1NNXzc3NTU2NDQ5OTUxM18wMDCUjEFTaW1fTEFNTVBTX0VBTV9Cb25ueUNhc3RpbkJ1bGxlbnNfMjAxM19GZUNyV19fU01fNjk5MjU3MzUwNzA0XzAwMJSMRFNpbV9MQU1NUFNfRUFNX0Jvbm55UGFzaWFub3RUZXJlbnR5ZXZfMjAxMV9GZUNyX19TTV8yMzcwODkyOTg0NjNfMDAwlIxAU2ltX0xBTU1QU19NRUFNX0FzYWRpWmFlZW1Ob3VyYW5pYW5fMjAxNV9GZV9fU01fMDQyNjMwNjgwOTkzXzAwMJSMOVNpbV9MQU1NUFNfTUVBTV9FdGVzYW1pQXNhZGlfMjAxOF9GZV9fU01fMjY3MDE2NjA4NzU1XzAwMJSMS1NpbV9MQU1NUFNfTUVBTV9KZWxpbmVrR3JvaEhvcnN0ZW1leWVyXzIwMTJfQWxTaU1nQ3VGZV9fU01fNjU2NTE3MzUyNDg1XzAwMJSMOlNpbV9MQU1NUFNfTUVBTV9LaW1KdW5nTGVlXzIwMDlfRmVUaUNfX1NNXzUzMTAzODI3NDQ3MV8wMDCUjD5TaW1fTEFNTVBTX01FQU1fTGl5YW5hZ2VLaW1Ib3V6ZV8yMDE0X0ZlQ19fU01fNjUyNDI1Nzc3ODA4XzAwMJSMSFNpbV9MQU1NUFNfUmVheEZGX0FyeWFucG91clZhbkR1aW5LdWJpY2tpXzIwMTBfRmVIT19fU01fMjIyOTY0MjE2MDAxXzAwMZSMRVNpbV9MQU1NUFNfVGVyc29mZlpCTF9CeWdnbWFzdGFyR3JhbmJlcmdfMjAyMF9GZV9fU01fOTU4ODYzODk1MjM0XzAwMJSMTVNpbV9MQU1NUFNfVGVyc29mZlpCTF9IZW5yaWtzc29uQmpvcmthc05vcmRsdW5kXzIwMTNfRmVDX19TTV80NzM0NjM0OTgyNjlfMDAwlGV1Lg=="
                             }
                         ],
                         "outputs": [
                             {
                                 "type": "exec",
-                                "label": "",
-                                "GID": 26
+                                "label": "ran",
+                                "GID": 32
                             },
                             {
                                 "type": "data",
                                 "label": "job",
-                                "GID": 27
+                                "GID": 33,
+                                "dtype": "DType.Data",
+                                "dtype state": "gASVgwAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjB9weWlyb25fYXRvbWlzdGljcy5sYW1tcHMubGFtbXBzlIwGTGFtbXBzlJOUYYwKYWxsb3dfbm9uZZSJjARzaXpllIwBbZR1Lg=="
                             }
                         ],
-                        "GID": 19,
-                        "pos x": 894.4829998343243,
-                        "pos y": 182.49336870026525
+                        "GID": 25,
+                        "pos x": 928.8226869091951,
+                        "pos y": 274.72527472527474
                     },
                     {
                         "identifier": "GenericOutput_Node",
@@ -136,144 +228,62 @@
                             {
                                 "type": "data",
                                 "label": "job",
-                                "GID": 29,
+                                "GID": 35,
                                 "dtype": "DType.Data",
-                                "dtype state": "gASVNQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjARzaXpllIwBbZR1Lg=="
+                                "dtype state": "gASVmwAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjCpweWlyb25fYXRvbWlzdGljcy5hdG9taXN0aWNzLmpvYi5hdG9taXN0aWOUjBNBdG9taXN0aWNHZW5lcmljSm9ilJOUYYwKYWxsb3dfbm9uZZSJjARzaXpllIwBbZR1Lg=="
                             },
                             {
                                 "type": "data",
                                 "label": "field",
-                                "GID": 30,
+                                "GID": 36,
                                 "val": "gASVGgAAAAAAAACMFklucHV0IGFuIGF0b21pc3RpYyBqb2KULg==",
                                 "dtype": "DType.Choice",
-                                "dtype state": "gASVZwAAAAAAAAB9lCiMB2RlZmF1bHSUjBZJbnB1dCBhbiBhdG9taXN0aWMgam9ilIwDdmFslGgCjANkb2OUjACUjAZib3VuZHOUTowFaXRlbXOUXZSMFklucHV0IGFuIGF0b21pc3RpYyBqb2KUYXUu"
+                                "dtype state": "gASVhAAAAAAAAAB9lCiMB2RlZmF1bHSUjBZJbnB1dCBhbiBhdG9taXN0aWMgam9ilIwDdmFslGgCjANkb2OUjACUjAZib3VuZHOUTowNdmFsaWRfY2xhc3Nlc5RdlIwIYnVpbHRpbnOUjANzdHKUk5RhjAphbGxvd19ub25llImMBWl0ZW1zlF2UaAJhdS4="
                             }
                         ],
                         "outputs": [
                             {
                                 "type": "data",
                                 "label": "output",
-                                "GID": 31
-                            }
-                        ],
-                        "GID": 28,
-                        "pos x": 1227.4248476704158,
-                        "pos y": 280.106100795756
-                    },
-                    {
-                        "identifier": "BulkStructure_Node",
-                        "version": null,
-                        "state data": "gAR9lC4=",
-                        "additional data": {},
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "element",
-                                "GID": 59,
-                                "val": "gASVBgAAAAAAAACMAkFslC4=",
-                                "dtype": "DType.Char",
-                                "dtype state": "gASVLwAAAAAAAAB9lCiMB2RlZmF1bHSUjAJGZZSMA3ZhbJRoAowDZG9jlIwAlIwGYm91bmRzlE51Lg=="
-                            },
-                            {
-                                "type": "data",
-                                "label": "crystal_structure",
-                                "GID": 60,
-                                "val": "gAROLg==",
-                                "dtype": "DType.Choice",
-                                "dtype state": "gASVlwAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjAVpdGVtc5RdlChOjAJzY5SMA2ZjY5SMA2JjY5SMA2hjcJSMB2RpYW1vbmSUjAp6aW5jYmxlbmRllIwIcm9ja3NhbHSUjA5jZXNpdW1jaGxvcmlkZZSMCGZsdW9yaXRllIwId3VydHppdGWUZXUu"
-                            },
-                            {
-                                "type": "data",
-                                "label": "a",
-                                "GID": 61,
-                                "val": "gAROLg==",
-                                "dtype": "DType.Float",
-                                "dtype state": "gASVNwAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjAhkZWNpbWFsc5RLCnUu"
-                            },
-                            {
-                                "type": "data",
-                                "label": "c",
-                                "GID": 62,
-                                "val": "gAROLg==",
-                                "dtype": "DType.Float",
-                                "dtype state": "gASVNwAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjAhkZWNpbWFsc5RLCnUu"
-                            },
-                            {
-                                "type": "data",
-                                "label": "c_over_a",
-                                "GID": 63,
-                                "val": "gAROLg==",
-                                "dtype": "DType.Float",
-                                "dtype state": "gASVNwAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjAhkZWNpbWFsc5RLCnUu"
-                            },
-                            {
-                                "type": "data",
-                                "label": "u",
-                                "GID": 64,
-                                "val": "gAROLg==",
-                                "dtype": "DType.Float",
-                                "dtype state": "gASVNwAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjAhkZWNpbWFsc5RLCnUu"
-                            },
-                            {
-                                "type": "data",
-                                "label": "orthorhombic",
-                                "GID": 65,
-                                "val": "gASJLg==",
-                                "dtype": "DType.Boolean",
-                                "dtype state": "gASVKgAAAAAAAAB9lCiMB2RlZmF1bHSUiYwDdmFslImMA2RvY5SMAJSMBmJvdW5kc5ROdS4="
-                            },
-                            {
-                                "type": "data",
-                                "label": "cubic",
-                                "GID": 66,
-                                "val": "gASILg==",
-                                "dtype": "DType.Boolean",
-                                "dtype state": "gASVKgAAAAAAAAB9lCiMB2RlZmF1bHSUiYwDdmFslImMA2RvY5SMAJSMBmJvdW5kc5ROdS4="
+                                "GID": 37
                             }
                         ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "structure",
-                                "GID": 67
-                            }
-                        ],
-                        "GID": 58,
-                        "pos x": 332.24996306692276,
-                        "pos y": 109.97163120567376
+                        "GID": 34,
+                        "pos x": 1349.0117444858206,
+                        "pos y": 342.85714285714283
                     }
                 ],
                 "connections": [
                     {
-                        "GID": 33,
+                        "GID": 38,
                         "parent node index": 0,
                         "output port index": 0,
-                        "connected node": 2,
-                        "connected input port index": 2
-                    },
-                    {
-                        "GID": 36,
-                        "parent node index": 2,
-                        "output port index": 1,
                         "connected node": 3,
-                        "connected input port index": 0
+                        "connected input port index": 4
                     },
                     {
-                        "GID": 68,
-                        "parent node index": 4,
+                        "GID": 39,
+                        "parent node index": 0,
                         "output port index": 0,
                         "connected node": 2,
-                        "connected input port index": 4
+                        "connected input port index": 0
                     },
                     {
-                        "GID": 69,
-                        "parent node index": 4,
+                        "GID": 40,
+                        "parent node index": 1,
                         "output port index": 0,
-                        "connected node": 1,
+                        "connected node": 3,
+                        "connected input port index": 2
+                    },
+                    {
+                        "GID": 41,
+                        "parent node index": 3,
+                        "output port index": 1,
+                        "connected node": 4,
                         "connected input port index": 0
                     }
                 ],
-                "GID": 6
+                "GID": 7
             },
             "GID": 1
         },
@@ -292,38 +302,38 @@
                             {
                                 "type": "data",
                                 "label": "min",
-                                "GID": 44,
+                                "GID": 50,
                                 "val": "gARLAS4=",
                                 "dtype": "DType.Integer",
-                                "dtype state": "gASVMQAAAAAAAAB9lCiMB2RlZmF1bHSUSwGMA3ZhbJRLAYwDZG9jlIwAlIwGYm91bmRzlEsAS2SGlHUu"
+                                "dtype state": "gASVZQAAAAAAAAB9lCiMB2RlZmF1bHSUSwGMA3ZhbJRLAYwDZG9jlIwAlIwGYm91bmRzlEsAS2SGlIwNdmFsaWRfY2xhc3Nlc5RdlIwIYnVpbHRpbnOUjANpbnSUk5RhjAphbGxvd19ub25llIl1Lg=="
                             },
                             {
                                 "type": "data",
                                 "label": "max",
-                                "GID": 45,
-                                "val": "gARLBC4=",
+                                "GID": 51,
+                                "val": "gARLAi4=",
                                 "dtype": "DType.Integer",
-                                "dtype state": "gASVMQAAAAAAAAB9lCiMB2RlZmF1bHSUSwKMA3ZhbJRLAowDZG9jlIwAlIwGYm91bmRzlEsAS2SGlHUu"
+                                "dtype state": "gASVZQAAAAAAAAB9lCiMB2RlZmF1bHSUSwKMA3ZhbJRLAowDZG9jlIwAlIwGYm91bmRzlEsAS2SGlIwNdmFsaWRfY2xhc3Nlc5RdlIwIYnVpbHRpbnOUjANpbnSUk5RhjAphbGxvd19ub25llIl1Lg=="
                             },
                             {
                                 "type": "data",
                                 "label": "steps",
-                                "GID": 46,
+                                "GID": 52,
                                 "val": "gARLCi4=",
                                 "dtype": "DType.Integer",
-                                "dtype state": "gASVMQAAAAAAAAB9lCiMB2RlZmF1bHSUSwqMA3ZhbJRLCowDZG9jlIwAlIwGYm91bmRzlEsBS2SGlHUu"
+                                "dtype state": "gASVZQAAAAAAAAB9lCiMB2RlZmF1bHSUSwqMA3ZhbJRLCowDZG9jlIwAlIwGYm91bmRzlEsBS2SGlIwNdmFsaWRfY2xhc3Nlc5RdlIwIYnVpbHRpbnOUjANpbnSUk5RhjAphbGxvd19ub25llIl1Lg=="
                             }
                         ],
                         "outputs": [
                             {
                                 "type": "data",
                                 "label": "linspace",
-                                "GID": 47
+                                "GID": 53
                             }
                         ],
-                        "GID": 43,
-                        "pos x": 154.3765992305285,
-                        "pos y": 322.54641909814325
+                        "GID": 49,
+                        "pos x": 163.24262389000287,
+                        "pos y": 314.2857142857143
                     },
                     {
                         "identifier": "Sin_Node",
@@ -334,21 +344,21 @@
                             {
                                 "type": "data",
                                 "label": "x",
-                                "GID": 49,
+                                "GID": 55,
                                 "dtype": "DType.Data",
-                                "dtype state": "gASVNQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjARzaXpllIwBbZR1Lg=="
+                                "dtype state": "gASVlQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UKIwIYnVpbHRpbnOUjANpbnSUk5RoCIwFZmxvYXSUk5RoCIwEbGlzdJSTlIwFbnVtcHmUjAduZGFycmF5lJOUZYwKYWxsb3dfbm9uZZSJjARzaXpllIwBbZR1Lg=="
                             }
                         ],
                         "outputs": [
                             {
                                 "type": "data",
                                 "label": "sin",
-                                "GID": 50
+                                "GID": 56
                             }
                         ],
-                        "GID": 48,
-                        "pos x": 491.5597444912836,
-                        "pos y": 496.55172413793105
+                        "GID": 54,
+                        "pos x": 605.4311085648811,
+                        "pos y": 529.6703296703297
                     },
                     {
                         "identifier": "Matplot_Node",
@@ -359,56 +369,58 @@
                             {
                                 "type": "data",
                                 "label": "x",
-                                "GID": 52,
+                                "GID": 58,
                                 "dtype": "DType.Data",
-                                "dtype state": "gASVNQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjARzaXpllIwBbZR1Lg=="
+                                "dtype state": "gASVfwAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UKIwIYnVpbHRpbnOUjARsaXN0lJOUjAVudW1weZSMB25kYXJyYXmUk5RljAphbGxvd19ub25llImMBHNpemWUjAFtlHUu"
                             },
                             {
                                 "type": "data",
                                 "label": "y",
-                                "GID": 53,
+                                "GID": 59,
                                 "dtype": "DType.Data",
-                                "dtype state": "gASVNQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjARzaXpllIwBbZR1Lg=="
+                                "dtype state": "gASVfwAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UKIwIYnVpbHRpbnOUjARsaXN0lJOUjAVudW1weZSMB25kYXJyYXmUk5RljAphbGxvd19ub25llImMBHNpemWUjAFtlHUu"
                             }
                         ],
                         "outputs": [
                             {
                                 "type": "data",
                                 "label": "fig",
-                                "GID": 54
+                                "GID": 60,
+                                "dtype": "DType.Data",
+                                "dtype state": "gASVdQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjBFtYXRwbG90bGliLmZpZ3VyZZSMBkZpZ3VyZZSTlGGMCmFsbG93X25vbmWUiYwEc2l6ZZSMAW2UdS4="
                             }
                         ],
-                        "GID": 51,
-                        "pos x": 809.6570513410526,
-                        "pos y": 345.8885941644562
+                        "GID": 57,
+                        "pos x": 924.4228014895446,
+                        "pos y": 353.84615384615387
                     }
                 ],
                 "connections": [
                     {
-                        "GID": 55,
+                        "GID": 61,
                         "parent node index": 0,
                         "output port index": 0,
                         "connected node": 1,
                         "connected input port index": 0
                     },
                     {
-                        "GID": 56,
+                        "GID": 62,
                         "parent node index": 0,
                         "output port index": 0,
                         "connected node": 2,
                         "connected input port index": 0
                     },
                     {
-                        "GID": 57,
+                        "GID": 63,
                         "parent node index": 1,
                         "output port index": 0,
                         "connected node": 2,
                         "connected input port index": 1
                     }
                 ],
-                "GID": 42
+                "GID": 48
             },
-            "GID": 37
+            "GID": 42
         }
     ]
 }
\ No newline at end of file
diff --git a/notebooks/example_table.ipynb b/notebooks/example_table.ipynb
index 67b3e42e..5d2307f9 100644
--- a/notebooks/example_table.ipynb
+++ b/notebooks/example_table.ipynb
@@ -13,22 +13,11 @@
    "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": "52e5ff9edccf4326abf2e448a20f77da",
+       "model_id": "6a61e55a92ba4a729801f6df83935083",
        "version_major": 2,
        "version_minor": 0
       },
@@ -36,12 +25,24 @@
      },
      "metadata": {},
      "output_type": "display_data"
-    },
+    }
+   ],
+   "source": [
+    "%matplotlib inline\n",
+    "from ironflow import GUI"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "e8da0476-4a99-4da3-8fe3-e12c646893e8",
+   "metadata": {},
+   "outputs": [
     {
-     "name": "stdout",
+     "name": "stderr",
      "output_type": "stream",
      "text": [
-      "Loaded session data for example\n"
+      "2022-11-25 15:38:58,670 - pyiron_log - WARNING - The job table is being loaded instead of running. To re-run use the argument 'delete_existing_job=True in create_job'\n"
      ]
     }
    ],
@@ -58,12 +59,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "34d01f28aa6449bd82cce1ea4dc054c0",
+       "model_id": "8ddb753b5a9f4760ae7bc0558f48a498",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "VBox(children=(HBox(children=(Dropdown(layout=Layout(width='80px'), options=('data', 'exec'), value='data'), B…"
+       "Tab(children=(VBox(children=(HBox(children=(Dropdown(layout=Layout(width='80px'), options=('data', 'exec'), va…"
       ]
      },
      "metadata": {},
@@ -112,7 +113,7 @@
     "class JobTable_Node(Node):\n",
     "    title = \"JobTable\"\n",
     "    init_inputs = [\n",
-    "        NodeInputBP(dtype=dtypes.Data(default=1), label=\"Project\")\n",
+    "        NodeInputBP(dtype=dtypes.Data(valid_classes=Project), label=\"project\")\n",
     "    ]\n",
     "    init_outputs = [\n",
     "       NodeOutputBP(label=\"Table\")\n",
@@ -120,7 +121,11 @@
     "    color = 'cyan'\n",
     "\n",
     "    def update_event(self, inp=-1):\n",
-    "        self.set_output_val(0, self.input(0).job_table(all_columns=False))        \n",
+    "        if self.inputs.ports.project.valid_val:\n",
+    "            self.set_output_val(\n",
+    "                0, \n",
+    "                self.inputs.values.project.job_table(all_columns=False)\n",
+    "            )        \n",
     "        \n",
     "        \n",
     "class PyironTable_Node(Node):\n",
@@ -130,8 +135,8 @@
     "    init_inputs = [\n",
     "        NodeInputBP(type_=\"exec\", label=\"run\"),\n",
     "        NodeInputBP(type_=\"exec\", label=\"remove\"),\n",
-    "        NodeInputBP(dtype=dtypes.Data(size=\"m\"), label=\"project\"),\n",
-    "        NodeInputBP(dtype=dtypes.Char(default=\"table\"), label=\"name\")          \n",
+    "        NodeInputBP(dtype=dtypes.Data(valid_classes=Project), label=\"project\"),\n",
+    "        NodeInputBP(dtype=dtypes.String(default=\"table\"), label=\"name\")          \n",
     "    ]\n",
     "    for n in np.arange(n_cols):\n",
     "        init_inputs.append(\n",
@@ -146,19 +151,10 @@
     "    for n in np.arange(n_cols):\n",
     "        init_outputs.append(NodeOutputBP(label=f\"Col_{n+1}\"))\n",
     "        \n",
-    "    color = 'cyan'\n",
-    "    \n",
-    "    \n",
-    "    @property\n",
-    "    def _project(self):\n",
-    "        return self.input(2)\n",
-    "\n",
-    "    @property\n",
-    "    def _name(self):\n",
-    "        return self.input(3)    \n",
+    "    color = 'cyan'  \n",
     "    \n",
     "    def _run(self):\n",
-    "        self.table = self._project.create.table(self._name)\n",
+    "        self.table = self.inputs.values.project.create.table(self.inputs.values.name)\n",
     "        self._job = self.table\n",
     "        for n in np.arange(self.n_cols):\n",
     "            getattr(self.table.add, self.inputs[n + 4].val)\n",
@@ -174,7 +170,7 @@
     "            pass    \n",
     "    \n",
     "    def _update_column_choices(self):\n",
-    "        self.table = self.input(2).create.table()\n",
+    "        self.table = self.inputs.values.project.create.table()\n",
     "        for n in np.arange(self.n_cols):\n",
     "            col_input = self.inputs[n + 4]\n",
     "            last_col = col_input.val\n",
@@ -185,7 +181,7 @@
     "            col_input.dtype.items = available_cols\n",
     "\n",
     "    def update_event(self, inp=-1):\n",
-    "        if inp == 0:\n",
+    "        if inp == 0 and self.all_input_is_valid:\n",
     "            self._run()\n",
     "            self.exec_output(0)\n",
     "        elif inp == 1:\n",
@@ -193,11 +189,14 @@
     "        elif inp > 1:\n",
     "            self._update_column_choices()              \n",
     "        \n",
-    "        df = self.table.get_dataframe()\n",
-    "        self.set_output_val(0, df)\n",
-    "        if len(df) > 0:\n",
-    "            for n in np.arange(self.n_cols):\n",
-    "                self.set_output_val(n+1, df.iloc[:, n+1])\n",
+    "        try:\n",
+    "            df = self.table.get_dataframe()\n",
+    "            self.set_output_val(0, df)\n",
+    "            if len(df) > 0:\n",
+    "                for n in np.arange(self.n_cols):\n",
+    "                    self.set_output_val(n+1, df.iloc[:, n+1])\n",
+    "        except AttributeError:\n",
+    "            pass\n",
     "\n",
     "                \n",
     "class ProjectSelector_Node(Node):\n",
@@ -241,43 +240,7 @@
     "        if self.project is not None:\n",
     "            self.set_output_val(0, self.project)\n",
     "            self.set_output_val(1, self.df_murn)\n",
-    "            \n",
-    "                    \n",
-    "class Plot2d_Node(Node):\n",
-    "    \"\"\"\n",
-    "    A 2D matplotlib plot.\n",
-    "\n",
-    "    Inputs:\n",
-    "        x pandas series: Data for the x-axis.\n",
-    "        y pandas series: Data for the y-axis.\n",
-    "\n",
-    "    Outputs:\n",
-    "        fig (matplotlib.figure.Figure): The resulting figure after a `matplotlib.pyplot.plot` call on x and y.\n",
-    "    \"\"\"\n",
-    "\n",
-    "    title = \"Plot2d\"\n",
-    "    version = \"v0.1\"\n",
-    "    init_inputs = [\n",
-    "        NodeInputBP(dtype=dtypes.Data(size=\"m\"), label=\"x\"),\n",
-    "        NodeInputBP(dtype=dtypes.Data(size=\"m\"), label=\"y\"),\n",
-    "    ]\n",
-    "    init_outputs = [\n",
-    "        NodeOutputBP(type_=\"data\", label=\"fig\"),\n",
-    "    ]\n",
-    "    color = \"#5d95de\"\n",
-    "\n",
-    "    def update_event(self, inp=-1):\n",
-    "        super().update_event()\n",
-    "        plt.ioff()\n",
-    "        fig = plt.figure()\n",
-    "        plt.clf()\n",
-    "        x = self.input(0)\n",
-    "        y = self.input(1)\n",
-    "        plt.plot(x.values, y.values)\n",
-    "        plt.xlabel(x.name)\n",
-    "        plt.ylabel(y.name)\n",
-    "        self.set_output_val(0, fig)\n",
-    "        plt.ion()        "
+    "                  "
    ]
   },
   {
@@ -290,37 +253,29 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 7,
    "id": "7f753105-1669-4163-a338-6b7887b820b2",
    "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "Loaded session data for example_table\n"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
-    "gui2 = GUI(\"example_table\", extra_nodes_packages=[[My_Node, JobTable_Node, PyironTable_Node, Plot2d_Node, ProjectSelector_Node]])"
+    "gui2 = GUI(\"example_table\", extra_nodes_packages=[[My_Node, JobTable_Node, PyironTable_Node, ProjectSelector_Node]])"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 8,
    "id": "61bd0c78-a148-483a-b232-f1e51e2ef2d7",
    "metadata": {},
    "outputs": [
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "39383e1c3a714b299cbe20275d569e0f",
+       "model_id": "48e895ef686f48cba40589eb1fa42e47",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "VBox(children=(HBox(children=(Dropdown(layout=Layout(width='80px'), options=('data', 'exec'), value='data'), B…"
+       "Tab(children=(VBox(children=(HBox(children=(Dropdown(layout=Layout(width='80px'), options=('data', 'exec'), va…"
       ]
      },
      "metadata": {},
diff --git a/notebooks/example_table.json b/notebooks/example_table.json
index f4f92139..ec1a3a79 100644
--- a/notebooks/example_table.json
+++ b/notebooks/example_table.json
@@ -7,90 +7,33 @@
                 "algorithm mode": "data",
                 "nodes": [
                     {
-                        "identifier": "My_Node",
-                        "version": null,
-                        "state data": "gAR9lC4=",
-                        "additional data": {},
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "foo",
-                                "GID": 795,
-                                "val": "gARLAS4=",
-                                "dtype": "DType.Integer",
-                                "dtype state": "gASVLAAAAAAAAAB9lCiMB2RlZmF1bHSUSwGMA3ZhbJRLAYwDZG9jlIwAlIwGYm91bmRzlE51Lg=="
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "bar",
-                                "GID": 796
-                            }
-                        ],
-                        "GID": 794,
-                        "pos x": 285.33018171074013,
-                        "pos y": 266.6666666666667
-                    },
-                    {
-                        "identifier": "IntRand_Node",
+                        "identifier": "Project_Node",
                         "version": null,
                         "state data": "gAR9lC4=",
                         "additional data": {},
                         "inputs": [
                             {
                                 "type": "data",
-                                "label": "high",
-                                "GID": 798,
-                                "dtype": "DType.Integer",
-                                "dtype state": "gASVMQAAAAAAAAB9lCiMB2RlZmF1bHSUSwGMA3ZhbJRLAYwDZG9jlIwAlIwGYm91bmRzlEsKS2SGlHUu"
-                            },
-                            {
-                                "type": "data",
-                                "label": "length",
-                                "GID": 799,
-                                "dtype": "DType.Integer",
-                                "dtype state": "gASVMQAAAAAAAAB9lCiMB2RlZmF1bHSUSwGMA3ZhbJRLAYwDZG9jlIwAlIwGYm91bmRzlEsBS2SGlHUu"
+                                "label": "name",
+                                "GID": 80,
+                                "val": "gASVBQAAAAAAAACMAS6ULg==",
+                                "dtype": "DType.String",
+                                "dtype state": "gASVbQAAAAAAAAB9lCiMB2RlZmF1bHSUjAEulIwDdmFslGgCjANkb2OUjACUjAZib3VuZHOUTowNdmFsaWRfY2xhc3Nlc5RdlIwIYnVpbHRpbnOUjANzdHKUk5RhjAphbGxvd19ub25llImMBHNpemWUjAFtlHUu"
                             }
                         ],
                         "outputs": [
                             {
                                 "type": "data",
-                                "label": "randint",
-                                "GID": 800
+                                "label": "project",
+                                "GID": 81,
+                                "dtype": "DType.Data",
+                                "dtype state": "gASVfgAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjBlweWlyb25fYXRvbWlzdGljcy5wcm9qZWN0lIwHUHJvamVjdJSTlGGMCmFsbG93X25vbmWUiYwEc2l6ZZSMAW2UdS4="
                             }
                         ],
-                        "GID": 797,
-                        "pos x": 648.3971044467424,
-                        "pos y": 292.1985815602837
-                    }
-                ],
-                "connections": [
-                    {
-                        "GID": 801,
-                        "parent node index": 0,
-                        "output port index": 0,
-                        "connected node": 1,
-                        "connected input port index": 0
+                        "GID": 79,
+                        "pos x": 110.44399885419651,
+                        "pos y": 292.3076923076923
                     },
-                    {
-                        "GID": 802,
-                        "parent node index": 0,
-                        "output port index": 0,
-                        "connected node": 1,
-                        "connected input port index": 1
-                    }
-                ],
-                "GID": 793
-            },
-            "GID": 788
-        },
-        {
-            "title": "Table",
-            "variables": {},
-            "flow": {
-                "algorithm mode": "data",
-                "nodes": [
                     {
                         "identifier": "JobTable_Node",
                         "version": null,
@@ -99,48 +42,22 @@
                         "inputs": [
                             {
                                 "type": "data",
-                                "label": "Project",
-                                "GID": 810,
+                                "label": "project",
+                                "GID": 83,
                                 "dtype": "DType.Data",
-                                "dtype state": "gASVNwAAAAAAAAB9lCiMB2RlZmF1bHSUSwGMA3ZhbJRLAYwDZG9jlIwAlIwGYm91bmRzlE6MBHNpemWUjAFtlHUu"
+                                "dtype state": "gASVfgAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjBlweWlyb25fYXRvbWlzdGljcy5wcm9qZWN0lIwHUHJvamVjdJSTlGGMCmFsbG93X25vbmWUiYwEc2l6ZZSMAW2UdS4="
                             }
                         ],
                         "outputs": [
                             {
                                 "type": "data",
                                 "label": "Table",
-                                "GID": 811
+                                "GID": 84
                             }
                         ],
-                        "GID": 809,
-                        "pos x": 929.044357469015,
-                        "pos y": 229.9296707776985
-                    },
-                    {
-                        "identifier": "Project_Node",
-                        "version": null,
-                        "state data": "gAR9lC4=",
-                        "additional data": {},
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "name",
-                                "GID": 813,
-                                "val": "gASVVQAAAAAAAACMUS4uLy4uLy4uL215anVweXRlcm5vdGVib29rcy9weWlyb24vRGV2ZWxvcG1lbnRzL09udG9sb2d5L2VsYXN0aWMvbGFtbXBzL211cm5faGRmNZQu",
-                                "dtype": "DType.Char",
-                                "dtype state": "gASVLgAAAAAAAAB9lCiMB2RlZmF1bHSUjAEulIwDdmFslGgCjANkb2OUjACUjAZib3VuZHOUTnUu"
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "project",
-                                "GID": 814
-                            }
-                        ],
-                        "GID": 812,
-                        "pos x": 327.87018917155905,
-                        "pos y": 387.1844528250603
+                        "GID": 82,
+                        "pos x": 528.4331137209969,
+                        "pos y": 145.05494505494505
                     },
                     {
                         "identifier": "PyironTable_Node",
@@ -151,70 +68,70 @@
                             {
                                 "type": "exec",
                                 "label": "run",
-                                "GID": 816,
+                                "GID": 87,
                                 "val": "gAROLg=="
                             },
                             {
                                 "type": "exec",
                                 "label": "remove",
-                                "GID": 817,
+                                "GID": 88,
                                 "val": "gAROLg=="
                             },
                             {
                                 "type": "data",
                                 "label": "project",
-                                "GID": 818,
+                                "GID": 89,
                                 "dtype": "DType.Data",
-                                "dtype state": "gASVNQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjARzaXpllIwBbZR1Lg=="
+                                "dtype state": "gASVfgAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjBlweWlyb25fYXRvbWlzdGljcy5wcm9qZWN0lIwHUHJvamVjdJSTlGGMCmFsbG93X25vbmWUiYwEc2l6ZZSMAW2UdS4="
                             },
                             {
                                 "type": "data",
                                 "label": "name",
-                                "GID": 819,
+                                "GID": 90,
                                 "val": "gASVCQAAAAAAAACMBXRhYmxllC4=",
-                                "dtype": "DType.Char",
-                                "dtype state": "gASVMgAAAAAAAAB9lCiMB2RlZmF1bHSUjAV0YWJsZZSMA3ZhbJRoAowDZG9jlIwAlIwGYm91bmRzlE51Lg=="
+                                "dtype": "DType.String",
+                                "dtype state": "gASVcQAAAAAAAAB9lCiMB2RlZmF1bHSUjAV0YWJsZZSMA3ZhbJRoAowDZG9jlIwAlIwGYm91bmRzlE6MDXZhbGlkX2NsYXNzZXOUXZSMCGJ1aWx0aW5zlIwDc3RylJOUYYwKYWxsb3dfbm9uZZSJjARzaXpllIwBbZR1Lg=="
                             },
                             {
                                 "type": "data",
                                 "label": "Col_1",
-                                "GID": 820,
-                                "val": "gASVEAAAAAAAAACMDGdldF9qb2JfbmFtZZQu",
+                                "GID": 91,
+                                "val": "gASVEgAAAAAAAACMDmdldF9lbmVyZ3lfdG90lC4=",
                                 "dtype": "DType.Choice",
-                                "dtype state": "gASVEAMAAAAAAAB9lCiMB2RlZmF1bHSUjAxnZXRfam9iX25hbWWUjAN2YWyUaAKMA2RvY5SMAJSMBmJvdW5kc5ROjAVpdGVtc5RdlCiMEWdldF9hdmVyYWdlX3dhdmVzlIwVZ2V0X2NvbnZlcmdlbmNlX2NoZWNrlIwKZ2V0X2VfYmFuZJSMEGdldF9lX2NvbnZfbGV2ZWyUjA5nZXRfZWtpbl9lcnJvcpSMDGdldF9lbGVtZW50c5SMCWdldF9lbmN1dJSMD2dldF9lbmVyZ3lfZnJlZZSMGGdldF9lbmVyZ3lfZnJlZV9wZXJfYXRvbZSMDmdldF9lbmVyZ3lfaW50lIwXZ2V0X2VuZXJneV9pbnRfcGVyX2F0b22UjA5nZXRfZW5lcmd5X3BvdJSMF2dldF9lbmVyZ3lfcG90X3Blcl9hdG9tlIwOZ2V0X2VuZXJneV90b3SUjBdnZXRfZW5lcmd5X3RvdF9wZXJfYXRvbZSMGmdldF9lcXVpbGlicml1bV9wYXJhbWV0ZXJzlIwMZ2V0X2Zfc3RhdGVzlIwKZ2V0X2ZvcmNlc5SMCWdldF9pbmNhcpSMCmdldF9pc21lYXKUjApnZXRfam9iX2lklIwMZ2V0X2pvYl9uYW1llIwWZ2V0X21hZ25ldGljX3N0cnVjdHVyZZSMHmdldF9tYWpvcml0eV9jcnlzdGFsX3N0cnVjdHVyZZSMFGdldF9tYWpvcml0eV9zcGVjaWVzlIwOZ2V0X25fZXF1X2twdHOUjApnZXRfbl9rcHRzlIwkZ2V0X251bWJlcl9vZl9maW5hbF9lbGVjdHJvbmljX3N0ZXBzlIwZZ2V0X251bWJlcl9vZl9pb25pY19zdGVwc5SMFWdldF9udW1iZXJfb2Zfc3BlY2llc5SMD2dldF9wbGFuZV93YXZlc5SMCWdldF9zaWdtYZSMDWdldF9zdHJ1Y3R1cmWUjBlnZXRfdG90YWxfbnVtYmVyX29mX2F0b21zlIwKZ2V0X3ZvbHVtZZSME2dldF92b2x1bWVfcGVyX2F0b22UZXUu"
+                                "dtype state": "gASVIwMAAAAAAAB9lCiMB2RlZmF1bHSUjAxnZXRfam9iX25hbWWUjAN2YWyUaAKMA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjAphbGxvd19ub25llImMBWl0ZW1zlF2UKIwRZ2V0X2F2ZXJhZ2Vfd2F2ZXOUjBVnZXRfY29udmVyZ2VuY2VfY2hlY2uUjApnZXRfZV9iYW5klIwQZ2V0X2VfY29udl9sZXZlbJSMDmdldF9la2luX2Vycm9ylIwMZ2V0X2VsZW1lbnRzlIwJZ2V0X2VuY3V0lIwPZ2V0X2VuZXJneV9mcmVllIwYZ2V0X2VuZXJneV9mcmVlX3Blcl9hdG9tlIwOZ2V0X2VuZXJneV9pbnSUjBdnZXRfZW5lcmd5X2ludF9wZXJfYXRvbZSMDmdldF9lbmVyZ3lfcG90lIwXZ2V0X2VuZXJneV9wb3RfcGVyX2F0b22UjA5nZXRfZW5lcmd5X3RvdJSMF2dldF9lbmVyZ3lfdG90X3Blcl9hdG9tlIwaZ2V0X2VxdWlsaWJyaXVtX3BhcmFtZXRlcnOUjAxnZXRfZl9zdGF0ZXOUjApnZXRfZm9yY2VzlIwJZ2V0X2luY2FylIwKZ2V0X2lzbWVhcpSMCmdldF9qb2JfaWSUaAKMFmdldF9tYWduZXRpY19zdHJ1Y3R1cmWUjB5nZXRfbWFqb3JpdHlfY3J5c3RhbF9zdHJ1Y3R1cmWUjBRnZXRfbWFqb3JpdHlfc3BlY2llc5SMDmdldF9uX2VxdV9rcHRzlIwKZ2V0X25fa3B0c5SMJGdldF9udW1iZXJfb2ZfZmluYWxfZWxlY3Ryb25pY19zdGVwc5SMGWdldF9udW1iZXJfb2ZfaW9uaWNfc3RlcHOUjBVnZXRfbnVtYmVyX29mX3NwZWNpZXOUjA9nZXRfcGxhbmVfd2F2ZXOUjAlnZXRfc2lnbWGUjA1nZXRfc3RydWN0dXJllIwZZ2V0X3RvdGFsX251bWJlcl9vZl9hdG9tc5SMCmdldF92b2x1bWWUjBNnZXRfdm9sdW1lX3Blcl9hdG9tlGV1Lg=="
                             },
                             {
                                 "type": "data",
                                 "label": "Col_2",
-                                "GID": 821,
-                                "val": "gASVEAAAAAAAAACMDGdldF9qb2JfbmFtZZQu",
+                                "GID": 92,
+                                "val": "gASVEgAAAAAAAACMDmdldF9lbmVyZ3lfcG90lC4=",
                                 "dtype": "DType.Choice",
-                                "dtype state": "gASVEAMAAAAAAAB9lCiMB2RlZmF1bHSUjAxnZXRfam9iX25hbWWUjAN2YWyUaAKMA2RvY5SMAJSMBmJvdW5kc5ROjAVpdGVtc5RdlCiMEWdldF9hdmVyYWdlX3dhdmVzlIwVZ2V0X2NvbnZlcmdlbmNlX2NoZWNrlIwKZ2V0X2VfYmFuZJSMEGdldF9lX2NvbnZfbGV2ZWyUjA5nZXRfZWtpbl9lcnJvcpSMDGdldF9lbGVtZW50c5SMCWdldF9lbmN1dJSMD2dldF9lbmVyZ3lfZnJlZZSMGGdldF9lbmVyZ3lfZnJlZV9wZXJfYXRvbZSMDmdldF9lbmVyZ3lfaW50lIwXZ2V0X2VuZXJneV9pbnRfcGVyX2F0b22UjA5nZXRfZW5lcmd5X3BvdJSMF2dldF9lbmVyZ3lfcG90X3Blcl9hdG9tlIwOZ2V0X2VuZXJneV90b3SUjBdnZXRfZW5lcmd5X3RvdF9wZXJfYXRvbZSMGmdldF9lcXVpbGlicml1bV9wYXJhbWV0ZXJzlIwMZ2V0X2Zfc3RhdGVzlIwKZ2V0X2ZvcmNlc5SMCWdldF9pbmNhcpSMCmdldF9pc21lYXKUjApnZXRfam9iX2lklIwMZ2V0X2pvYl9uYW1llIwWZ2V0X21hZ25ldGljX3N0cnVjdHVyZZSMHmdldF9tYWpvcml0eV9jcnlzdGFsX3N0cnVjdHVyZZSMFGdldF9tYWpvcml0eV9zcGVjaWVzlIwOZ2V0X25fZXF1X2twdHOUjApnZXRfbl9rcHRzlIwkZ2V0X251bWJlcl9vZl9maW5hbF9lbGVjdHJvbmljX3N0ZXBzlIwZZ2V0X251bWJlcl9vZl9pb25pY19zdGVwc5SMFWdldF9udW1iZXJfb2Zfc3BlY2llc5SMD2dldF9wbGFuZV93YXZlc5SMCWdldF9zaWdtYZSMDWdldF9zdHJ1Y3R1cmWUjBlnZXRfdG90YWxfbnVtYmVyX29mX2F0b21zlIwKZ2V0X3ZvbHVtZZSME2dldF92b2x1bWVfcGVyX2F0b22UZXUu"
+                                "dtype state": "gASVIwMAAAAAAAB9lCiMB2RlZmF1bHSUjAxnZXRfam9iX25hbWWUjAN2YWyUaAKMA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjAphbGxvd19ub25llImMBWl0ZW1zlF2UKIwRZ2V0X2F2ZXJhZ2Vfd2F2ZXOUjBVnZXRfY29udmVyZ2VuY2VfY2hlY2uUjApnZXRfZV9iYW5klIwQZ2V0X2VfY29udl9sZXZlbJSMDmdldF9la2luX2Vycm9ylIwMZ2V0X2VsZW1lbnRzlIwJZ2V0X2VuY3V0lIwPZ2V0X2VuZXJneV9mcmVllIwYZ2V0X2VuZXJneV9mcmVlX3Blcl9hdG9tlIwOZ2V0X2VuZXJneV9pbnSUjBdnZXRfZW5lcmd5X2ludF9wZXJfYXRvbZSMDmdldF9lbmVyZ3lfcG90lIwXZ2V0X2VuZXJneV9wb3RfcGVyX2F0b22UjA5nZXRfZW5lcmd5X3RvdJSMF2dldF9lbmVyZ3lfdG90X3Blcl9hdG9tlIwaZ2V0X2VxdWlsaWJyaXVtX3BhcmFtZXRlcnOUjAxnZXRfZl9zdGF0ZXOUjApnZXRfZm9yY2VzlIwJZ2V0X2luY2FylIwKZ2V0X2lzbWVhcpSMCmdldF9qb2JfaWSUaAKMFmdldF9tYWduZXRpY19zdHJ1Y3R1cmWUjB5nZXRfbWFqb3JpdHlfY3J5c3RhbF9zdHJ1Y3R1cmWUjBRnZXRfbWFqb3JpdHlfc3BlY2llc5SMDmdldF9uX2VxdV9rcHRzlIwKZ2V0X25fa3B0c5SMJGdldF9udW1iZXJfb2ZfZmluYWxfZWxlY3Ryb25pY19zdGVwc5SMGWdldF9udW1iZXJfb2ZfaW9uaWNfc3RlcHOUjBVnZXRfbnVtYmVyX29mX3NwZWNpZXOUjA9nZXRfcGxhbmVfd2F2ZXOUjAlnZXRfc2lnbWGUjA1nZXRfc3RydWN0dXJllIwZZ2V0X3RvdGFsX251bWJlcl9vZl9hdG9tc5SMCmdldF92b2x1bWWUjBNnZXRfdm9sdW1lX3Blcl9hdG9tlGV1Lg=="
                             }
                         ],
                         "outputs": [
                             {
                                 "type": "data",
                                 "label": "Table",
-                                "GID": 822
+                                "GID": 93
                             },
                             {
                                 "type": "data",
                                 "label": "Col_1",
-                                "GID": 823
+                                "GID": 94
                             },
                             {
                                 "type": "data",
                                 "label": "Col_2",
-                                "GID": 824
+                                "GID": 95
                             }
                         ],
-                        "GID": 815,
-                        "pos x": 930.4707033373193,
-                        "pos y": 461.3327897812203
+                        "GID": 86,
+                        "pos x": 565.8321397880263,
+                        "pos y": 386.8131868131868
                     },
                     {
-                        "identifier": "Plot2d_Node",
+                        "identifier": "Matplot_Node",
                         "version": "v0.1",
                         "state data": "gAR9lC4=",
                         "additional data": {},
@@ -222,63 +139,53 @@
                             {
                                 "type": "data",
                                 "label": "x",
-                                "GID": 834,
+                                "GID": 98,
+                                "val": "gAROLg==",
                                 "dtype": "DType.Data",
-                                "dtype state": "gASVNQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjARzaXpllIwBbZR1Lg=="
+                                "dtype state": "gASVfwAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UKIwIYnVpbHRpbnOUjARsaXN0lJOUjAVudW1weZSMB25kYXJyYXmUk5RljAphbGxvd19ub25llImMBHNpemWUjAFtlHUu"
                             },
                             {
                                 "type": "data",
                                 "label": "y",
-                                "GID": 835,
+                                "GID": 99,
+                                "val": "gAROLg==",
                                 "dtype": "DType.Data",
-                                "dtype state": "gASVNQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjARzaXpllIwBbZR1Lg=="
+                                "dtype state": "gASVfwAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UKIwIYnVpbHRpbnOUjARsaXN0lJOUjAVudW1weZSMB25kYXJyYXmUk5RljAphbGxvd19ub25llImMBHNpemWUjAFtlHUu"
                             }
                         ],
                         "outputs": [
                             {
                                 "type": "data",
                                 "label": "fig",
-                                "GID": 836
+                                "GID": 100,
+                                "dtype": "DType.Data",
+                                "dtype state": "gASVdQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjA12YWxpZF9jbGFzc2VzlF2UjBFtYXRwbG90bGliLmZpZ3VyZZSMBkZpZ3VyZZSTlGGMCmFsbG93X25vbmWUiYwEc2l6ZZSMAW2UdS4="
                             }
                         ],
-                        "GID": 833,
-                        "pos x": 1371.771037181996,
-                        "pos y": 475.93035028709266
+                        "GID": 97,
+                        "pos x": 1058.6193067888858,
+                        "pos y": 334.0659340659341
                     }
                 ],
                 "connections": [
                     {
-                        "GID": 829,
-                        "parent node index": 1,
+                        "GID": 85,
+                        "parent node index": 0,
                         "output port index": 0,
-                        "connected node": 0,
+                        "connected node": 1,
                         "connected input port index": 0
                     },
                     {
-                        "GID": 830,
-                        "parent node index": 1,
+                        "GID": 96,
+                        "parent node index": 0,
                         "output port index": 0,
                         "connected node": 2,
                         "connected input port index": 2
-                    },
-                    {
-                        "GID": 837,
-                        "parent node index": 2,
-                        "output port index": 1,
-                        "connected node": 3,
-                        "connected input port index": 1
-                    },
-                    {
-                        "GID": 838,
-                        "parent node index": 2,
-                        "output port index": 2,
-                        "connected node": 3,
-                        "connected input port index": 0
                     }
                 ],
-                "GID": 808
+                "GID": 71
             },
-            "GID": 803
+            "GID": 65
         }
     ]
 }
\ No newline at end of file
diff --git a/notebooks/example_with_custom_node.json b/notebooks/example_with_custom_node.json
index c2a81e9f..90b746aa 100644
--- a/notebooks/example_with_custom_node.json
+++ b/notebooks/example_with_custom_node.json
@@ -15,22 +15,22 @@
                             {
                                 "type": "data",
                                 "label": "foo",
-                                "GID": 66,
-                                "val": "gARLAi4=",
+                                "GID": 73,
+                                "val": "gARLAS4=",
                                 "dtype": "DType.Integer",
-                                "dtype state": "gASVLAAAAAAAAAB9lCiMB2RlZmF1bHSUSwGMA3ZhbJRLAYwDZG9jlIwAlIwGYm91bmRzlE51Lg=="
+                                "dtype state": "gASVYAAAAAAAAAB9lCiMB2RlZmF1bHSUSwGMA3ZhbJRLAYwDZG9jlIwAlIwGYm91bmRzlE6MDXZhbGlkX2NsYXNzZXOUXZSMCGJ1aWx0aW5zlIwDaW50lJOUYYwKYWxsb3dfbm9uZZSJdS4="
                             }
                         ],
                         "outputs": [
                             {
                                 "type": "data",
                                 "label": "bar",
-                                "GID": 67
+                                "GID": 74
                             }
                         ],
-                        "GID": 65,
-                        "pos x": 285.33018171074013,
-                        "pos y": 266.6666666666667
+                        "GID": 72,
+                        "pos x": 175.82781456953643,
+                        "pos y": 229.8507462686567
                     },
                     {
                         "identifier": "IntRand_Node",
@@ -41,49 +41,49 @@
                             {
                                 "type": "data",
                                 "label": "high",
-                                "GID": 69,
+                                "GID": 76,
                                 "dtype": "DType.Integer",
-                                "dtype state": "gASVMQAAAAAAAAB9lCiMB2RlZmF1bHSUSwGMA3ZhbJRLAYwDZG9jlIwAlIwGYm91bmRzlEsKS2SGlHUu"
+                                "dtype state": "gASVZQAAAAAAAAB9lCiMB2RlZmF1bHSUSwGMA3ZhbJRLAYwDZG9jlIwAlIwGYm91bmRzlEsKS2SGlIwNdmFsaWRfY2xhc3Nlc5RdlIwIYnVpbHRpbnOUjANpbnSUk5RhjAphbGxvd19ub25llIl1Lg=="
                             },
                             {
                                 "type": "data",
                                 "label": "length",
-                                "GID": 70,
+                                "GID": 77,
                                 "dtype": "DType.Integer",
-                                "dtype state": "gASVMQAAAAAAAAB9lCiMB2RlZmF1bHSUSwGMA3ZhbJRLAYwDZG9jlIwAlIwGYm91bmRzlEsBS2SGlHUu"
+                                "dtype state": "gASVZQAAAAAAAAB9lCiMB2RlZmF1bHSUSwGMA3ZhbJRLAYwDZG9jlIwAlIwGYm91bmRzlEsBS2SGlIwNdmFsaWRfY2xhc3Nlc5RdlIwIYnVpbHRpbnOUjANpbnSUk5RhjAphbGxvd19ub25llIl1Lg=="
                             }
                         ],
                         "outputs": [
                             {
                                 "type": "data",
                                 "label": "randint",
-                                "GID": 71
+                                "GID": 78
                             }
                         ],
-                        "GID": 68,
-                        "pos x": 648.3971044467424,
-                        "pos y": 292.1985815602837
+                        "GID": 75,
+                        "pos x": 665.9654940158541,
+                        "pos y": 274.6268656716418
                     }
                 ],
                 "connections": [
                     {
-                        "GID": 72,
+                        "GID": 79,
                         "parent node index": 0,
                         "output port index": 0,
                         "connected node": 1,
                         "connected input port index": 0
                     },
                     {
-                        "GID": 73,
+                        "GID": 80,
                         "parent node index": 0,
                         "output port index": 0,
                         "connected node": 1,
                         "connected input port index": 1
                     }
                 ],
-                "GID": 64
+                "GID": 71
             },
-            "GID": 59
+            "GID": 65
         }
     ]
 }
\ No newline at end of file


Test patch:
diff --git a/tests/unit/model/__init__.py b/tests/unit/model/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/unit/model/test_dtypes.py b/tests/unit/model/test_dtypes.py
new file mode 100644
index 00000000..423ecfbc
--- /dev/null
+++ b/tests/unit/model/test_dtypes.py
@@ -0,0 +1,88 @@
+# coding: utf-8
+# Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department
+# Distributed under the terms of "New BSD License", see the LICENSE file.
+
+from unittest import TestCase
+
+from ryvencore.utils import deserialize, serialize
+
+from ironflow.model import dtypes
+
+
+class TestDTypes(TestCase):
+    def test_from_string(self):
+        self.assertEqual(dtypes.Data, dtypes.DType.from_str("DType.Data"))
+
+    def test_serialization(self):
+        default = "foo"
+        items = ["bar", default]
+        allow_none = True
+        valid_classes = str
+        choice = dtypes.Choice(
+            default=default,
+            items=items,
+            allow_none=allow_none,
+            valid_classes=valid_classes
+        )
+        reloaded = dtypes.Choice(_load_state=deserialize(serialize(choice.get_state())))
+        self.assertEqual(choice.default, reloaded.default)
+        self.assertEqual(choice.items, reloaded.items)
+        self.assertEqual(choice.allow_none, reloaded.allow_none)
+        self.assertEqual(choice.valid_classes, reloaded.valid_classes)
+
+    def test_matching(self):
+        with self.subTest("Simple types"):
+            d = dtypes.Integer()
+            self.assertTrue(d.matches(dtypes.Integer()), msg="DTypes should match")
+            self.assertTrue(d.matches(7), msg="Value should match")
+            self.assertFalse(d.matches(dtypes.String), msg="DTypes should not match")
+            self.assertFalse(d.matches([]), msg="Value should not match")
+
+        classes1 = [int, float]
+        classes2 = [int, float, str]
+        for D in [dtypes.Choice, dtypes.Data]:
+            with self.subTest(
+                    f"Test valid classes as subsets for {d.__class__.__name__}"
+            ):
+                self.assertTrue(
+                    D(valid_classes=classes2).matches(D(valid_classes=classes1)),
+                    msg="DTypes should match when classes are a subset"
+                )
+                self.assertFalse(
+                    D(valid_classes=classes1).matches(D(valid_classes=classes2)),
+                    msg="DTypes should not match when classes are a superset"
+                )
+            self.assertTrue(
+                dtypes.Data(valid_classes=int).matches(7),
+                msg="Value should match"
+            )
+            self.assertFalse(
+                dtypes.Data(valid_classes=int).matches("foo"),
+                msg="Value should not match"
+            )
+
+        with self.subTest("Ensure specificity works only within dtypes"):
+            self.assertFalse(
+                dtypes.Choice(valid_classes=classes1).matches(
+                    dtypes.Data(valid_classes=classes1)
+                )
+            )
+
+        with self.subTest("Test choices"):
+            d = dtypes.Choice(default="foo", items=["bar", "foo"])
+            self.assertTrue(d.matches("bar"))
+            self.assertFalse(d.matches("not an item"))
+
+        for D in [
+            dtypes.Boolean,
+            dtypes.Char,
+            dtypes.Choice,
+            dtypes.Data,
+            dtypes.Data,
+            dtypes.Float,
+            dtypes.Integer,
+            dtypes.List,
+            dtypes.String
+        ]:
+            with self.subTest(f"Test None for {d.__class__.__name__}"):
+                self.assertTrue(D(allow_none=True).matches(None))
diff --git a/tests/unit/model/test_flow.py b/tests/unit/model/test_flow.py
new file mode 100644
index 00000000..d05e6f60
--- /dev/null
+++ b/tests/unit/model/test_flow.py
@@ -0,0 +1,66 @@
+# coding: utf-8
+# Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department
+# Distributed under the terms of "New BSD License", see the LICENSE file.
+
+from unittest import TestCase
+
+from ironflow.model.dtypes import Integer, String
+from ironflow.model.port import NodeInput, NodeOutput
+from ironflow.model.session import Session
+
+
+class FakeNode:
+    def __init__(self, title):
+        self.title = title
+
+
+class TestFlow(TestCase):
+    @classmethod
+    def setUpClass(cls) -> None:
+        cls.session = Session()
+        cls.session.create_script()
+        cls.flow = cls.session.scripts[0].flow
+
+    def test_check_connection_validity(self):
+        i0 = NodeInput(FakeNode("input"))
+        i1 = NodeInput(FakeNode("input"), dtype=Integer(allow_none=True))
+        i2 = NodeInput(FakeNode("input"), dtype=Integer(allow_none=False))
+
+        o0 = NodeOutput(FakeNode("output"))
+        o0.val = None
+        o1 = NodeOutput(FakeNode("output"))
+        o1.val = 42
+        o2 = NodeOutput(FakeNode("output"), dtype=Integer(allow_none=True))
+        o2.val = 42
+        o3 = NodeOutput(FakeNode("output"), dtype=Integer(allow_none=False))
+        o3.val = 42
+        o4 = NodeOutput(FakeNode("output"), dtype=String())
+
+        expectations = (
+            (i0, o0, True),
+            (i0, o1, True),
+            (i0, o2, True),
+            (i0, o3, True),
+            (i0, o4, True),
+            (i1, o0, True),
+            (i1, o1, True),
+            (i1, o2, True),
+            (i1, o3, True),
+            (i1, o4, False),  # Wrong dtype
+            (i2, o0, False),  # Value is None
+            (i2, o1, True),
+            (i2, o2, False),  # Other dtype not specific enough (allows None)
+            (i2, o3, True),
+            (i2, o4, False),  # Wrong dtype
+        )
+
+        for e in expectations:
+            self.assertEqual(
+                e[2],
+                self.flow.check_connection_validity(e[0], e[1]),
+                msg=f"Input with dtype {e[0].dtype} and allow_none "
+                    f"{e[0].dtype.allow_none if hasattr(e[0].dtype, 'allow_none') else None} compared {e[2]} "
+                    f"against output with dtype {e[1].dtype}, allow_none "
+                    f"{e[1].dtype.allow_none if hasattr(e[1].dtype, 'allow_none') else None}, and value "
+                    f"{e[1].val}"
+            )
diff --git a/tests/unit/model/test_port.py b/tests/unit/model/test_port.py
new file mode 100644
index 00000000..0320a7c7
--- /dev/null
+++ b/tests/unit/model/test_port.py
@@ -0,0 +1,44 @@
+# coding: utf-8
+# Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department
+# Distributed under the terms of "New BSD License", see the LICENSE file.
+
+from unittest import TestCase
+
+from ironflow.model.dtypes import String
+from ironflow.model.port import NodeInput, NodeOutput, NodeOutputBP
+
+
+class TestPorts(TestCase):
+    def test_for_dtype(self):
+        """The `dtype` attribute should always be present, although it may be None"""
+        self.assertTrue(hasattr(NodeInput(node=None), 'dtype'))
+        self.assertTrue(hasattr(NodeOutput(node=None), 'dtype'))
+        self.assertTrue(hasattr(NodeOutputBP(), 'dtype'))
+
+    def test_validity(self):
+        with self.subTest("Without dtype set"):
+            p0 = NodeInput(node=None, dtype=None)
+            p0.val = None
+            self.assertTrue(p0.valid_val)
+            p0.val = "foo"
+            self.assertTrue(p0.valid_val)
+            p0.val = 42
+            self.assertTrue(p0.valid_val)
+
+        with self.subTest("With allow none"):
+            p0 = NodeInput(node=None, dtype=String(allow_none=True))
+            p0.val = None
+            self.assertTrue(p0.valid_val)
+            p0.val = "foo"
+            self.assertTrue(p0.valid_val)
+            p0.val = 42
+            self.assertFalse(p0.valid_val, msg="Should be wrong type")
+
+        with self.subTest("With allow none"):
+            p0 = NodeInput(node=None, dtype=String(allow_none=False))
+            p0.val = None
+            self.assertFalse(p0.valid_val, msg="None should be disallowed")
+            p0.val = "foo"
+            self.assertTrue(p0.valid_val)
+            p0.val = 42
+            self.assertFalse(p0.valid_val, msg="Should be wrong type")
diff --git a/tests/unit/test_flow_canvas.py b/tests/unit/test_flow_canvas.py
index d3ea844c..373966a5 100644
--- a/tests/unit/test_flow_canvas.py
+++ b/tests/unit/test_flow_canvas.py
@@ -10,7 +10,7 @@
 class TestCanvasObect(TestCase):
 
     def setUp(self):
-        self.gui = GUI('gui')
+        self.gui = GUI('gui', log_to_display=False)
         self.canvas = self.gui.workflows.flow_canvas
 
     @classmethod
@@ -28,10 +28,19 @@ def test_remove_node_from_flow(self):
 
         self.canvas.add_node(0, 0, val_node)
         self.canvas.add_node(0, 0, results_node)
-        c12 = flow.connect_nodes(self.canvas.objects_to_draw[0].outputs[0], self.canvas.objects_to_draw[1].inputs[0])
+        c12 = flow.connect_nodes(
+            self.canvas.objects_to_draw[0].outputs[0],
+            self.canvas.objects_to_draw[1].inputs[0]
+        )
         self.assertEqual(2, len(flow.nodes))
         self.assertEqual(1, len(flow.connections))
         self.canvas.objects_to_draw[0].select()
         self.canvas.delete_selected()
-        self.assertEqual(1, len(flow.nodes), msg="Expected exactly one node to get deleted.")
-        self.assertEqual(0, len(flow.connections), msg="Expected the connection to get automatically deleted")
\ No newline at end of file
+        self.assertEqual(
+            1, len(flow.nodes), msg="Expected exactly one node to get deleted."
+        )
+        self.assertEqual(
+            0,
+            len(flow.connections),
+            msg="Expected the connection to get automatically deleted"
+        )
\ No newline at end of file