This README gives some info about what Pictograph is, and how it works.
Pictograph is a graphical interface for connecting and running small pieces of python code. At its core it is meant to provide a rich, graphical, way to create and run workflows, called pictographs. These pictographs are created by chaining together nodes or glyphs; it's a visual way to create computer programs. Each glyph represents a small piece of python code, and visually shows its inputs and output as anchors which can be connected to other nodes. Each input is connected to the output of a single other node, and each output can be connected to multiple nodes. The "ready state" of each node is recorded and automatically communicated to all connected nodes, thus allowing the computations represented by the pictograph to proceed automatically once all inputs are defined.
Pictographs are saved in json format for maximum openness and flexibility. Since json is plain text, you can do anything with them that you can do with a text file... like edit them by hand, put them in version control, copy/paste them into an email, etc.
Pictograph is completely written in python, both for speed of development and for maximum "hackability". The quickest way to get started extending Pictograph is to define your own node types. Node.py
defines the base Node
class, and your custom subclasses can do any computations that you desire. You define what your node does, and Pictograph turns it into a drag-and-drop black box for incorporating into your workflows.
Here's some ideas of what to do with Pictograph:
- Create computational workflows... visually
- Design and test new computational algorithms
- Create workflows for capturing data in the lab, and running postprocessing routines
- Define pictographs that take your data and automatically create polished graphs
- Anything else you could do with python!
Pictograph is currently only available in source format. You need a few things to get up and running with it.
- Some sort of python environment (Using Anaconda is strongly encouraged!)
- PyQt5 and numpy
Once you have your python environment set up, just run python main.py
!
The node base class defined in Node.py
is set up to provide nearly all the necessary functionality that makes pictographs work. The behavior of a node is described by a small set of functions, which you can think of as a mini API:
list_inputs()
: returns a list of names/keys of the inputs. In traditional programming, these would be the names of function arguments. Each input is another Node.connect_input( input_name, node )
: sets node to be the input forinput_name
disconnect_input( input_name )
: removes the connection forinput_name
connect_output( node )
: addsnode
to the list of output connectionsdisconnect_output( node )
: removesnode
from the list of output connectionsis_output_valid()
: check if cached output is accurate given the current values of the inputsprocess()
: do the computation that defines this nodeset_auto_process(flag)
: setauto_process
to value of True/False flag, to say if this node should automatically perform its process once its inputs become validdescription
: the text that describes this node
For the nerds who care (that's all of you, right?), the Node class works by message passing, using a simple implementation of the Observer design pattern. Each node is both a publisher (subject) and a subscriber (observer). When the node's output becomes valid (or invalid), it sends a message to all nodes connected to its output. Meanwhile it is subscribed to receive messages from all the nodes connected to its inputs. And that's pretty much it; not too complicated is it? Note that the Node class is implemented in a separate file from the rest of the app, so that if you want to use the nodes without the GUI, you totally can.
Also, the output of a node can be anything you want. Since objects in python are fundamental to how the language works, this means that nodes can be much more powerful than you might first imagine. (Want a node that returns the definition of a new python class? You can totally do that.)
Here are a couple examples of node definitions, so that you can see how easy it really is to write your own custom nodes.
class AdditionNode(Node):
def __init__(self):
super().__init__()
self.displayName = "Add"
self.description = "Adds two input values"
self._input_terminals = {"arg1": None, "arg2": None}
def _process_core(self):
return self.arg1 + self.arg2
Note in this example that the keys in the _input_terminals
dictionary become variables that you can use in the _process_core
function.
class NumberNode(Node):
def __init__(self, the_value=0):
super().__init__()
self.displayName = "Number"
self.description = "A constant numerical value"
self._adjustable_parameters =
{'Number': AdjustableParameter(
name="Number",
type="double",
val=the_value)}
self._output_data_cache = self._adjustable_parameters['Number']._value
self._is_output_valid = True
def _process_core(self):
self._output_data_cache = self._adjustable_parameters["Number"]._value
return self._output_data_cache
This example shows a node with "adjustable parameters". These are values that will have a widget in the GUI so that you can, unsurprisingly, adjust their values. For simple parameter types, the GUI widgets will be created automatically. For more complicated nodes, you can create your own QT widget for adjusting the node, displaying its value... whatever you want, really.