Acyclic Graphs
npm i acyclicgraph
Easy node tree graph for creating graphs like DAGs i.e. any arbitrary node tree with forward and backpropagation, repeaters, etc. for chaining scripts and scopes e.g. game systems. You can construct any type of graph and run async coroutines etc.
This is built around the idea of having an operator i.e. a custom i/o handler at each scope. You can easily extend the graphnode class with primitives etc for different systems, just read the code. Otherwise it's a pure javascript implementation with no dependencies.
Running a node returns a promise that resolves after the tree is finished running, so you can chain complex functions that can mutate e.g. an object returned and passed along by the first operator or a property on a node. See below for a very simple example.
This can get as complex as you want as each node is essentially just a different local scope and a main() function for each, with some easy piping based on writing a native object hierarchy with optional tagging for important nodes where results need to be subscribed to or chained to other complex nodes.
For a more basic function sequencer, see Sequencer.js
Basic usage
let tree = {
tag:'top',
operator:(
input,
node,
origin,
cmd
)=>{
if(typeof input === 'object') {
if(input?.x) node.x = input.x;
if(input?.y) node.y = input.y;
if(input?.z) node.z = input.z;
console.log('top node, input:', input);
}
return input;
},
forward:true,
x:3,
y:2,
z:1,
children:{
tag:'next',
operator:(
input,
node,
origin,
cmd
)=>{
if(origin.x) {
node.x = origin.x;
node.y = origin.y;
node.z = origin.z;
}
console.log('next node \n parent node:',node,'\ninput',input);
},
delay:500,
repeat:3
},
delay:1000
};
let graph = new AcyclicGraph();
graph.addNode(tree);
let res = graph.run(tree.tag,{x:4,y:5,z:6}).then(res => console.log('promise, after', res));
console.log('promise returned:',res);
Also try the webcomponents we built to run natively with our AcyclicGraph logic!
npm i acyclicgraph-webcomponents
Run the /example_app for demonstration, it's purely conceptual but you can see a fully implemented example at http://190.92.148.106 using this to do gravitational physics with html elements as planets.
GraphNode class
These are the objects created to represent each node in the tree. They can be created without belonging to an acyclic graph. The acyclic graph simply adds sequential tags 'node0, node1' etc (rather than random tags) to all untagged nodes according to the order of the tree provided so it's easier to create self-referencing trees.
GraphNode properties
type GraphNodeProperties = {
tag?:string,
operator:(
input:any,
node:GraphNode|string,
origin?:GraphNode|string,
cmd?:string|number
)=>any,
forward:boolean,
backward:boolean,
children?:string|GraphNodeProperties|GraphNode|(GraphNodeProperties|GraphNode|string)[],
parent?:GraphNode|undefined,
delay?:false|number,
repeat?:false|number,
recursive?:false|number,
frame?:boolean,
animate?:boolean,
loop?:false|number,
animation?:(
input:any,
node:GraphNode|string,
origin?:GraphNode|string,
cmd?:string|number
)=>any | undefined,
looper?:(
input:any,
node:GraphNode|string,
origin?:GraphNode|string,
cmd?:string|number
)=>any | undefined,
[key:string]:any
};
GraphNode utilities
let props={
operator:(
input,
node,
origin,
cmd
)=>{ console.log(input); return input; },
forward:true,
backward:false,
children:undefined,
parent:undefined,
delay:false,
repeat:false,
recursive:false,
frame:false,
animate:false,
loop:undefined,
tag:undefined,
};
let node = new GraphNode(props, parentNode, graph);
node
.operator(input,node=this,origin,cmd)
.runOp(input, node=this, origin, cmd)
.runNode(node,input,origin)
.run(input,node=this,origin)
.runAnimation(input,node=this,origin)
.runLoop(input,node=this,origin)
.setOperator(operator)
.setParent(parent)
.addChildren(children)
.removeTree(node)
.addNode(props)
.appendNode(props, parentNode=this)
.getNode(tag)
.stopLooping()
.stopAnimating()
.stopNode()
.convertChildrenToNodes(node=this)
.callParent(input, origin=this, cmd)
.callChildren(input, origin=this, cmd, idx)
.setProps(props)
.subscribe(callback=(res)=>{},tag=this.tag)
.unsubscribe(sub,tag=this.tag)
.subscribeNode(node)
.print(node=this,printChildren=true)
.reconstruct(json='{}')
Acyclic Graph Utilities
let graph = new AcyclicGraph();
graph
.addNode(node)
.getNode(tag)
.create(operator=(input,node,origin,cmd)=>{},parentNode,props)
.run(node,input,origin)
.runNode(node,input,origin)
.removeTree(node)
.removeNode(node)
.appendNode(node, parentNode)
.callParent(node,input,origin=node,cmd)
.callChildren(node, input, origin=node, cmd, idx)
.subscribe(tag, callback=(res)=>{})
.unsubscribe(tag, sub)
.subscribeNode(inputNode,outputNode)
.print(node,printChildren=true)
.reconstruct(json='{}')
Extra methods:
reconstructNode(json='{}')
createNode(operator=(input,node,origin,cmd)=>{},parentNode,props,graph)
Design Philosophy
Graphs simply are a way to manage operation sequences. These can be directed or undirected, and can have cycles on some nodes (technically not acyclic) with single trees or multiple running concurrently e.g. an animation loop and then event loops for user or server inputs
Acyclic graphs are trees of operations with arbitrary entry and exit points plus arbitrary propagation of results through the tree.
Each node is an object with a few required properties and functions and then anything else you want to add as variables, reference, utility functions etc.
Nodes added to the graph tree are made into a 'GraphNode' class object with some added utility functions added to allow generic message passing between parent/child/any nodes.
There are additional properties to indicate whether to delay (or render on frame), repeat or recurse, and do automatic forward or backprop based on the tree hierarchy.
Each node comes with an 'operator' main function to handle input and output with arbitrary conditions.
Tagged nodes are indexed as callable entry points to the tree.
Node operations return results via a promise as well as propagating up or down-treee (or to other trees) based on available default object settings.
All else will be built into the custom main 'operator()' functions you add yourself.
The 'operator()' function in each node is a program for that node that passes an input, the node, and the origin node if it's passing the input.
It can and should return results which can be used for propagation to other nodes automatically or for returning results from a chain of operations
starting with the called node. This is like a 'main()' program in a file where the node is the script's scope with local properties
Tagged node operation results can also be subscribed to with via an internal state manager from anywhere in your program so you don't need to add more lines to operators to output to certain places.
Contributors
Joshua Brewster -- AGPLv3.0